Serving Static Files

Stream Files From Disk to the Response

Serving Static Files

Serve files from disk safely — with the right Content-Type and no directory-traversal bugs.

3 min read Level 2/5 #nodejs#http#static
What you'll learn
  • Stream a file to res
  • Set Content-Type from extension
  • Avoid path-traversal

Serving a file from disk over HTTP — common, easy to get wrong.

The Naive Version

import { createServer } from "node:http";
import { createReadStream } from "node:fs";
import { join } from "node:path";

createServer((req, res) => {
  const path = join("./public", req.url);   // ⚠️ DANGEROUS
  createReadStream(path).pipe(res);
}).listen(3000);

Two problems:

  1. Path traversal: GET /../etc/passwd reads anywhere on disk.
  2. No Content-Type: browser guesses, sometimes wrong.

The Safer Version

import { createServer } from "node:http";
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { join, normalize, extname } from "node:path";

const ROOT = "./public";
const MIME = {
  ".html": "text/html; charset=utf-8",
  ".js":   "application/javascript",
  ".css":  "text/css",
  ".json": "application/json",
  ".png":  "image/png",
  ".jpg":  "image/jpeg",
  ".svg":  "image/svg+xml",
};

createServer(async (req, res) => {
  // 1. Normalize and validate path
  const requested = decodeURIComponent(req.url.split("?")[0]);
  const safePath = normalize(requested).replace(/^[\/\\]+/, "");

  if (safePath.includes("..")) {
    res.statusCode = 403;
    return res.end("forbidden");
  }

  const path = join(ROOT, safePath || "index.html");

  // 2. Make sure it exists and is a file
  try {
    const s = await stat(path);
    if (!s.isFile()) throw new Error("not a file");
  } catch {
    res.statusCode = 404;
    return res.end("not found");
  }

  // 3. Send with content type
  res.setHeader("content-type", MIME[extname(path)] ?? "application/octet-stream");
  createReadStream(path).pipe(res);
}).listen(3000);

The pattern: normalize → reject .. → stat → pipe.

In Real Code

You won’t write this. Use:

  • Expressexpress.static("./public")
  • Fastify@fastify/static
  • Nginx / Caddy — let the reverse proxy serve static files (faster, with caching)

For production: serve static files from a CDN or the edge. Node should serve dynamic content only.

Cookies →