Views and Static Files

By Syncfusion®, Inc. · Last updated

EJS templates, passing data, loops, conditions, partials and safe escaping, then serving CSS, images and scripts with express.static, caching, SPA fallbacks and keeping private files safe.

What you will learn

  • Render EJS templates with data
  • Use loops, conditions and partials
  • Escape output to stop XSS
  • Serve static files with express.static
  • Set caching and keep private files safe

Server-Side Rendering Introduction

Server-side rendering (SSR) = the server builds the full HTML page before sending it.

  • The browser gets a finished page. It does not need to build it.
  • Express builds pages from templates and data.
  • Template = an HTML file with gaps for data.
  • View engine = the tool that fills the gaps. This course uses EJS.
  • res.render("menu", data) fills views/menu.ejs and sends the HTML.

Simple lesson: A template plus data makes a page.

A Real-World Example

Every school report card has the same printed layout.

  1. The layout has empty boxes: name, class, marks.
  2. The office fills in one student's details.
  3. Each student gets the same layout with different details.
  4. Nobody draws a new report card from scratch.

The printed layout is the template. The details are the data. Filling the boxes is rendering.

In this lesson, you will learn:

  1. How Express builds HTML pages from EJS templates and data.
  2. Loops, conditions, partials and safe escaping.
  3. Custom view engines, and when server-side rendering fits.
  4. How to serve a folder with express.static.
  5. Mount paths, absolute paths and caching options.
  6. How to serve a built frontend app safely.

Static HTML vs Dynamic HTML

  • Static HTML = the same file for everyone. See the other part of this lesson.
  • Dynamic HTML = built per request, with today's data.
  • The canteen's "About us" page can be static.
  • The canteen's "Today's menu" page changes daily, so it is dynamic.

Animation: a template and data go into res.render, and a finished HTML page comes out

views/menu.ejs

data ["Idli", "Dosa"]

res.render()

HTML page

The template and the data meet in res.render. A finished page comes out.

Simple lesson: Static pages never change. Rendered pages change with the data.

Setting Up a View Engine

On your own computer, install EJS first:

npm install ejs
  • app.set("view engine", "ejs") = use EJS for files without an extension.
  • app.set("views", folder) = where templates live. Default: views in the project.
  • View file extension: .ejs. res.render("menu") finds views/menu.ejs.
  • You do not need to import ejs. Express loads it from the setting.

In this lab, EJS is already installed. On your own computer, npm install ejs adds it to package.json.

  • The response panel shows the rendered page. Press Source to see the HTML.
  • <%= school %> was replaced by Green Valley.

Simple lesson: Set the view engine, put templates in views/, call res.render.

EJS Tags

  • <%= value %> = print a value, escaped (safe).
  • <%- value %> = print a value unescaped (raw HTML).
  • <% code %> = run JavaScript, such as if and loops. Prints nothing.
  • <%# note %> = a comment. Not sent to the browser.
  • -%> = also removes the line break after the tag.

Simple lesson: <%= prints safely, <% runs code.

Passing Data to Templates

The template can read data from three places, merged together:

  • app.locals: shared by every page, such as the school name.
  • res.locals: for this request, such as the logged-in user.
  • The object in res.render: for this page, such as the menu.
  • If two have the same name, the res.render object wins.
  • school comes from app.locals, student from res.locals.
  • The forEach loop prints one <li> per item.
  • ?veg=true turns on the note and filters the list.

Simple lesson: Loops and if go in <% %>. Values go in <%= %>.

Reusable Views

Every page needs the same header and footer.

  • Partial = a small template used inside others, such as partials/header.ejs.
  • Include = <%- include("partials/header") %> puts a partial in place.
  • Use <%- for includes, because the partial is HTML you trust.
  • You can pass data: include("partials/header", { title: "Menu" }).
  • Layout = the shared page shape. In EJS, you build it with a header and a footer partial.
views/
├── home.ejs
├── about.ejs
└── partials/
    ├── header.ejs
    └── footer.ejs

Simple lesson: Write the header once, include it everywhere.

Template Safety

Some data comes from users, such as a name or a review.

  • A user might type <script>alert("hi")</script> as their name.
  • <%= name %> escapes it, so the browser shows the text and runs nothing.
  • <%- name %> prints it raw, so the script would run. This attack is XSS.
  • Use <%- only for HTML you wrote yourself, like includes.
  • Never build a template from user text. Only pass user text as data.
  • Press Source. The safe line shows &lt;script&gt;. The unsafe line has a real <script> tag.
  • The preview here blocks scripts. A real browser would run it.

Default to <%= %>. Every <%- %> with user data is a security hole.

Simple lesson: Escape user data. Always.

Template Errors and Custom Engines

  • A missing template gives Failed to lookup view. Express sends it to your error handler.
  • A typo inside a template, like an unknown variable, is also an error.
  • app.engine(ext, fn) = registers your own engine for a file extension.
  • The function gets (filePath, options, callback) and calls back with the HTML.
  • Express-compatible engines, such as EJS and Pug, already provide this function.
  • / fills the gaps with your own engine.
  • /missing shows the "Failed to lookup view" error.

Simple lesson: app.engine connects any template language to res.render.

Choosing a Template Engine

  • EJS: plain HTML with <% %> tags. Easiest if you know HTML. Used in this course.
  • Pug: short, indentation-based syntax. No closing tags.
  • Handlebars (express-handlebars): {{ }} tags, very little logic in templates.
  • All three escape output by default.
  • Pick one per project and stick with it.

When to Use Server-Side Rendering

  • Good for: content sites, blogs, admin pages, simple forms, pages that search engines must read.
  • Good because: the first page shows fast, and it works without much browser JavaScript.
  • Less good for: very interactive apps, like a live chat or a drawing tool.
  • Limits: every click may reload a full page, and the server does more work.
  • Many teams use React or Vue for rich apps, and Express only for the API.

Easy way to remember: content pages = SSR, app-like pages = a frontend framework plus an Express API.

Simple lesson: SSR is simple and fast to start. Choose it for content, not for complex apps.

Static Files

Static files = files sent exactly as they are saved.

  • CSS files, browser JavaScript, images, fonts and PDFs are static.
  • The server does not change them per request.
  • Dynamic responses are built per request, such as res.json or res.render.
  • express.static(folder) = built-in middleware that serves a whole folder.
  • The usual folder name is public.

Simple lesson: Put files in public/, and express.static hands them out.

A Second Example

The canteen has a notice board and a kitchen.

  1. The notice board holds printed papers: the timetable, the price list.
  2. Anyone can walk up and read them. Nobody cooks anything.
  3. The kitchen makes each order fresh.
  4. The staff never let visitors into the kitchen store room.

public/ is the notice board. Your routes are the kitchen. Your source code is the store room.

Serving a Public Folder

  • app.use(express.static("public")) serves everything in public.
  • public/style.css is served at /style.css. The folder name is not in the URL.
  • public/index.html is served at /. This is the index file.
  • Express picks the Content-Type from the file extension.
  • A missing file passes on to the next middleware, usually ending in 404.
  • Open Headers for each file and compare Content-Type.
  • /nothing.png falls through to the normal 404.

Simple lesson: The URL path matches the file's path inside public.

Mount Paths and Virtual Prefixes

  • Mount path: app.use("/assets", express.static("public")).
  • Now public/style.css is at /assets/style.css.
  • Virtual prefix = a URL prefix that is not a real folder name.
  • It keeps file URLs apart from your routes and API paths.
  • Change the prefix later without moving any files.
  • /assets/style.css works. /style.css now gives 404.

Simple lesson: A mount path adds a prefix to every static URL.

Multiple Folders and Absolute Paths

  • You can register more than one static folder.
  • Express checks them in order. The first folder with the file wins.
  • Relative path: "public" is relative to where you run node.
  • Run node from another folder, and "public" points to the wrong place.
  • Absolute path: path.join(import.meta.dirname, "public") always points next to your code file.
  • import.meta.dirname needs Node.js 20.11 or later.
  • /banner.txt comes from festival, because it is checked first.
  • /rules.txt only exists in public, so that folder answers.
  • Remove the festival line after Diwali. The normal banner comes back.

Easy way to remember: always build static paths with path.join(import.meta.dirname, "public").

Simple lesson: Order decides which folder wins. Absolute paths stop surprises.

Static Middleware Options

Pass options as the second argument: express.static("public", { maxAge: "1d" }).

  • index: the file for a folder URL. Default "index.html". false turns it off.
  • extensions: try these endings, so /about finds about.html.
  • dotfiles: files starting with a dot. Default "ignore": never served.
  • fallthrough: true (default) passes misses on. false sends an error.
  • maxAge: how long browsers may cache files, in milliseconds or text like "1d".
  • immutable: tells browsers the file will never change.
  • etag and lastModified: help browsers check if their copy is still fresh. Both on by default.
  • redirect: adds a trailing / to folder URLs. On by default.
  • setHeaders: a function to add your own headers per file.
  • /about finds about.html thanks to extensions.
  • Look at Cache-Control under Headers: max-age=86400.
  • /.env gives 404, because dotfiles are ignored by default.

Simple lesson: Options control file lookup, caching and extra headers.

Caching Static Files

Caching = the browser keeps a copy so it does not download the file again.

  • Cache-Control: max-age = how many seconds the copy stays fresh.
  • ETag = a short fingerprint of the file. If it matches, the server answers 304 Not Modified.
  • Short cache for files that change under the same name, like style.css.
  • Long cache + immutable for files with a version in the name, like app.3f9a1c.js.
  • Build tools such as Vite add these versions for you.

In this lab, the browser cache is not used, so every request reaches the server. Real browsers send If-None-Match with the ETag and may get a quick 304 instead of the whole file.

Simple lesson: Versioned file names can be cached for a year. Plain names should not be.

Serving a Built Frontend

A React or Vue app builds into a folder, often dist.

  • Serve dist with express.static.
  • Put API routes first, under /api.
  • Then add a fallback route that sends index.html for any other GET.
  • The fallback lets the frontend router handle URLs like /orders/7.
  • In Express 5, the fallback path is "/*splat", not "*".
  • /orders/7 is not a file, so it gets index.html.
  • /api/nope gets a JSON 404, not the HTML page. API mistakes stay clear.

Simple lesson: API first, static files second, index.html fallback last.

Static File Safety

  • Never serve your project root: express.static(".") exposes server.js and package.json.
  • Keep only public files in public. No .env, no database files, no notes.
  • Dotfiles are ignored by default. Do not change dotfiles to "allow".
  • Express blocks .. in paths, so visitors cannot climb out of the folder.
  • Private files, like student report cards, need a route with a login check. See File Handling.
  • /server.js sends your source code, password and all.
  • /.env is still blocked, but that is luck, not good design.
  • Change "." to "public" and create a public folder.

Anything inside a static folder is public. Ask "would I print this on the notice board?" before you put it there.

Simple lesson: Serve one folder made for the public, never the whole project.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
No view engine setExpress knows .ejsSet it, or write res.render("menu.ejs")No default engine was specified
Template outside views/Express searches everywhereIt looks in the views settingFailed to lookup view
<%- userName %>Raw is the same as escapedRaw prints HTML as codeXSS attacks
<%= include(...) %>Includes are valuesIncludes are HTML, use <%-The header shows as text
Forgetting npm install ejsExpress bundles EJSIt is a separate packageCannot find module 'ejs'
Per-user data in app.localsIt is per requestIt is shared by everyoneOne user's name shows to all
<link href="/public/style.css">The folder name is in the URLMount path + file path only404 for every asset
express.static("public") with a relative pathIt is relative to the code fileIt is relative to where node startedFiles missing when started elsewhere
express.static(".")Convenient during developmentIt exposes source codeSecrets leak
SPA fallback before API routesOrder does not matterAPI routes must come firstAPI calls get HTML
app.get("*", ...) fallbackExpress 4 syntax still worksUse "/*splat" in Express 5TypeError at startup
Long cache on style.cssCaching is always goodOnly for versioned namesUsers see old styles for days

Check Yourself

Exercises

What to Remember

  • SSR builds HTML on the server from a template plus data.
  • app.set("view engine", "ejs") and templates in views/.
  • res.render(view, data) fills the template and sends the page.
  • <%= %> prints safely, <% %> runs code, <%- %> is only for trusted HTML.
  • Use partials and include for shared headers and footers.
  • express.static("public") serves a folder. The folder name is not in the URL.
  • A mount path like /assets adds a prefix.
  • Options control index files, extensions, dotfiles and caching.
  • Never serve the project root. Only public files go in public.

Next: Input Processing and Validation. You will check and clean every value a user sends.