Skip to content
Laviyx Learn

Try

Node.js and Express course menu

Serving Static Files with Express (HTML, CSS, Images)

Serve a website's static files with Express: express.static, the public folder, index.html, mounting at a path prefix, absolute paths with import.meta.dirname, caching with maxAge, serving a single-page app with an API, and sending individual files with res.sendFile.

Intermediate6 min readUpdated
On this page

Static files are files sent to the browser exactly as they are on disk: HTML pages, CSS, JavaScript, images and fonts. Express serves a whole folder of them with one line: express.static.

The public folder#

public/index.html (HTML)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>ShopKart</title>
    <link rel="stylesheet" href="/styles.css">
  </head>
  <body>
    <h1>Welcome to ShopKart</h1>
    <p id="count">Loading products…</p>
    <script src="/app.js"></script>
  </body>
</html>
public/styles.css (CSS)
body { font-family: system-ui, sans-serif; margin: 2rem; }
public/app.js (JavaScript)
fetch("/api/products")
  .then((res) => res.json())
  .then((products) => {
    document.getElementById("count").textContent = `${products.length} products available`;
  });
server.js (JavaScript)
import express from "express";
import path from "node:path";

const app = express();

app.use(express.static(path.join(import.meta.dirname, "public")));

app.get("/api/products", (req, res) => {
  res.json([{ id: 1, name: "Notebook" }, { id: 2, name: "Gel pen" }]);
});

app.listen(3000, () => console.log("Open http://localhost:3000"));
Terminal
curl -s localhost:3000/ | grep "<h1>"
curl -s -i localhost:3000/styles.css | grep -E "^(HTTP|Content-Type)"
curl -s -i localhost:3000/app.js | grep -E "^(HTTP|Content-Type)"
curl -s localhost:3000/api/products
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/missing.png
Output
    <h1>Welcome to ShopKart</h1>
HTTP/1.1 200 OK
Content-Type: text/css; charset=utf-8
HTTP/1.1 200 OK
Content-Type: text/javascript; charset=utf-8
[{"id":1,"name":"Notebook"},{"id":2,"name":"Gel pen"}]
404

How it works:

  • A request for /styles.css is answered with public/styles.css, with the right Content-Type based on the extension.
  • / serves public/index.html automatically.
  • If no file matches, express.static calls next(), so your API routes and 404 handler still work.
  • Building the folder path from import.meta.dirname means it works no matter which folder you start node from.

Opening http://localhost:3000 in a browser loads the page, which then calls the API and shows "2 products available": a front end and a back end from one server.

Mounting at a path#

To serve files under a prefix, pass a path to app.use:

JavaScript
app.use("/static", express.static(path.join(import.meta.dirname, "public")));
// public/styles.css is now at /static/styles.css

Caching#

Browsers can cache static files to avoid downloading them again. maxAge sets the Cache-Control header:

JavaScript
app.use(express.static("public", { maxAge: "7d" }));

Long cache times are safe for files whose names change when their content changes (build tools add a hash: app.3f9a1c.js). Keep index.html uncached or short-lived so users get new versions.

Single-page apps (React, Vue)#

A React app built with Vite produces a dist/ folder. Serve it with express.static, and send index.html for any other non-API path so client-side routes (/products/3) work on reload:

JavaScript
const dist = path.join(import.meta.dirname, "dist");
app.use(express.static(dist));
app.get("/api/products", listProducts);              // API routes first
app.get("/*splat", (req, res) => res.sendFile(path.join(dist, "index.html")));   // everything else

res.sendFile sends one file and needs an absolute path. The /*splat pattern is Express 5's named wildcard.

In production, a CDN or the hosting platform often serves static files instead of Node, which frees the server for API requests.

Common mistakes#

Practice#

Practice exercise

Serve the public folder at /assets instead of /, and add a GET / route that sends a short HTML page linking to /assets/styles.css. Check with curl that /assets/styles.css is served and /styles.css is not.

Show hint

app.use("/assets", express.static(...)), and res.send("<link rel=stylesheet href=/assets/styles.css><h1>Shop</h1>") for the home route.

Show solution
assets.js (JavaScript)
import express from "express";
import path from "node:path";

const app = express();
app.use("/assets", express.static(path.join(import.meta.dirname, "public")));
app.get("/", (req, res) => res.send('<link rel="stylesheet" href="/assets/styles.css"><h1>Shop</h1>'));
app.listen(3000);
Terminal
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/assets/styles.css
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/styles.css
Output
200
404

Key takeaway#

Next: Building a REST API.

Finished this lesson?