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.
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#
<!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>body { font-family: system-ui, sans-serif; margin: 2rem; }fetch("/api/products")
.then((res) => res.json())
.then((products) => {
document.getElementById("count").textContent = `${products.length} products available`;
});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"));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 <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"}]
404How it works:
- A request for
/styles.cssis answered withpublic/styles.css, with the rightContent-Typebased on the extension. /servespublic/index.htmlautomatically.- If no file matches,
express.staticcallsnext(), so your API routes and 404 handler still work. - Building the folder path from
import.meta.dirnamemeans it works no matter which folder you startnodefrom.
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:
app.use("/static", express.static(path.join(import.meta.dirname, "public")));
// public/styles.css is now at /static/styles.cssCaching#
Browsers can cache static files to avoid downloading them again. maxAge sets the Cache-Control header:
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:
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 elseres.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
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);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.css200
404Key takeaway#
Next: Building a REST API.
Finished this lesson?