# Views and Static Files

Course: [Express.js](https://www.devtutorials.ai/learn/expressjs) · Section: Views and Static Content
Source: https://www.devtutorials.ai/learn/expressjs/views-static/server-side-views

> 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.

## 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.

<figure className="xfig ngc-fig">
<svg viewBox="0 0 560 160" role="img" aria-labelledby="xfig-ssr" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
  <title id="xfig-ssr">Animation: a template and data go into res.render, and a finished HTML page comes out</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    <rect x="10" y="14" width="150" height="54" rx="10" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 2 }} />
    <text x="85" y="36" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>views/menu.ejs</text>
    <text x="85" y="56" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontWeight: 400 }}>{"<li><%= item %></li>"}</text>
    <rect x="10" y="92" width="150" height="54" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
    <text x="85" y="114" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>data</text>
    <text x="85" y="134" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontWeight: 400 }}>["Idli", "Dosa"]</text>
    <rect x="215" y="50" width="130" height="60" rx="30" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 2 }} />
    <text x="280" y="85" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)" }}>res.render()</text>
    <rect x="400" y="30" width="150" height="100" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
    <text x="475" y="54" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>HTML page</text>
    <g opacity="0" style={{ fontFamily: "var(--mono)", fontWeight: 400 }}>
      <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.55;0.65;0.95;1" dur="6s" repeatCount="indefinite" />
      <text x="475" y="82" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>{"<li>Idli</li>"}</text>
      <text x="475" y="104" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>{"<li>Dosa</li>"}</text>
    </g>
  </g>
  <g style={{ stroke: "var(--line)", strokeWidth: 2, fill: "none" }}>
    <path d="M160 41 L215 72" /><path d="M160 119 L215 92" /><path d="M345 80 L400 80" />
  </g>
  <circle r="7" style={{ fill: "var(--ngc-purple)" }}>
    <animateMotion path="M160 41 L215 72" dur="6s" repeatCount="indefinite" keyPoints="0;1;1" keyTimes="0;0.25;1" calcMode="linear" />
  </circle>
  <circle r="7" style={{ fill: "var(--ngc-blue)" }}>
    <animateMotion path="M160 119 L215 92" dur="6s" repeatCount="indefinite" keyPoints="0;1;1" keyTimes="0;0.25;1" calcMode="linear" />
  </circle>
  <circle r="7" style={{ fill: "var(--ngc-orange)" }} opacity="0">
    <animateMotion path="M345 80 L400 80" dur="6s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.35;0.55;1" calcMode="linear" />
    <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.34;0.35;0.55;0.56;1" dur="6s" repeatCount="indefinite" />
  </circle>
</svg>
<figcaption>The template and the data meet in res.render. A finished page comes out.</figcaption>
</figure>

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

## Setting Up a View Engine

On your own computer, install EJS first:

```bash
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.

```text
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

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No `view engine` set | Express knows `.ejs` | Set it, or write `res.render("menu.ejs")` | `No default engine was specified` |
| Template outside `views/` | Express searches everywhere | It looks in the `views` setting | `Failed to lookup view` |
| `<%- userName %>` | Raw is the same as escaped | Raw prints HTML as code | XSS attacks |
| `<%= include(...) %>` | Includes are values | Includes are HTML, use `<%-` | The header shows as text |
| Forgetting `npm install ejs` | Express bundles EJS | It is a separate package | `Cannot find module 'ejs'` |
| Per-user data in `app.locals` | It is per request | It is shared by everyone | One user's name shows to all |
| `<link href="/public/style.css">` | The folder name is in the URL | Mount path + file path only | 404 for every asset |
| `express.static("public")` with a relative path | It is relative to the code file | It is relative to where node started | Files missing when started elsewhere |
| `express.static(".")` | Convenient during development | It exposes source code | Secrets leak |
| SPA fallback before API routes | Order does not matter | API routes must come first | API calls get HTML |
| `app.get("*", ...)` fallback | Express 4 syntax still works | Use `"/*splat"` in Express 5 | `TypeError` at startup |
| Long cache on `style.css` | Caching is always good | Only for versioned names | Users see old styles for days |

</div>

## 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.
