Static Files & Templating

beginner express static templating

Not every Express app is a JSON API. Sometimes we serve a full website — HTML pages, CSS files, images, client-side JS. Express has two built-in tools for this: express.static for files that don’t change, and view engines for HTML we generate from data.

In simple language: static = “send this file as-is”, templating = “fill in the blanks, then send.”

Static files — express.static

const path = require("path");

app.use(express.static(path.join(__dirname, "public")));

Now anything in public/ is reachable directly:

public/
  styles.css      →  GET /styles.css
  js/app.js       →  GET /js/app.js
  images/logo.png →  GET /images/logo.png
  favicon.ico     →  GET /favicon.ico

Notice there’s no /public prefix in the URL — Express strips the folder name. If we want a prefix:

app.use("/assets", express.static("public"));
// Now: /assets/styles.css → public/styles.css

Useful options

app.use(express.static("public", {
  maxAge: "1d",        // Cache-Control: max-age=86400
  etag: true,          // send ETag for conditional GETs
  index: "home.html",  // default file for directories (instead of index.html)
  dotfiles: "ignore",  // skip files starting with . (like .env)
}));

In production we usually slap a CDN or nginx in front of static assets, but express.static is perfect for dev or small apps.

Templating — server-rendered HTML

A view engine takes a template file with placeholders and data, and spits out HTML. Express supports many — the popular ones are EJS, Pug, and Handlebars. Pick one based on syntax preference.

EJS
HTML + <%= %>
familiar, no learning curve
Pug
indentation-based
terse but unique syntax
Handlebars
HTML + {{ }}
logic-less, popular

Setting up EJS

npm install ejs
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));  // default is ./views

app.get("/users/:id", async (req, res) => {
  const user = await db.findUser(req.params.id);
  res.render("user-profile", { user, title: "Profile" });
});

res.render(template, data) finds views/user-profile.ejs, runs it with the data, sends HTML.

views/user-profile.ejs

<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body>
    <h1>Hello, <%= user.name %></h1>
    <% if (user.isAdmin) { %>
      <a href="/admin">Admin Panel</a>
    <% } %>
    <ul>
      <% user.posts.forEach(post => { %>
        <li><%= post.title %></li>
      <% }); %>
    </ul>
  </body>
</html>

Three EJS tags to remember:

  • <%= value %> — print value, escaped (XSS-safe)
  • <%- value %> — print value, raw HTML (use sparingly)
  • <% code %> — execute JS, don’t print

Pug version

npm install pug
app.set("view engine", "pug");
doctype html
html
  head
    title= title
  body
    h1 Hello, #{user.name}
    if user.isAdmin
      a(href="/admin") Admin Panel
    ul
      each post in user.posts
        li= post.title

No closing tags, indentation defines nesting. Some love it, some hate it.

Handlebars version

npm install express-handlebars
const { engine } = require("express-handlebars");
app.engine("handlebars", engine());
app.set("view engine", "handlebars");
<h1>Hello, {{user.name}}</h1>
{{#if user.isAdmin}}
  <a href="/admin">Admin Panel</a>
{{/if}}
<ul>
  {{#each user.posts}}
    <li>{{this.title}}</li>
  {{/each}}
</ul>

Logic-less by design — keeps templates clean and forces business logic into the route handler.

Putting it all together

A typical server-rendered Express app:

const express = require("express");
const path = require("path");

const app = express();

// Serve CSS/JS/images
app.use(express.static(path.join(__dirname, "public")));

// HTML pages
app.set("view engine", "ejs");

app.get("/", (req, res) => {
  res.render("home", { title: "Welcome" });
});

app.get("/users/:id", async (req, res) => {
  const user = await db.findUser(req.params.id);
  res.render("user-profile", { user });
});

app.listen(3000);

For modern apps we’d usually go SPA (React/Vue) + JSON API. But for blogs, dashboards, or admin panels, server-rendered templates are still the fastest way to ship.