cheat sheet
serve
Package-level reference for serve on npm — install, SPA fallback, auth, CORS, and when to reach for caddy or python -m http.server instead.
serve
What it is
serve is a zero-config static-file HTTP server from Vercel, designed for the "I just built a SPA and want to verify it locally" workflow. Point it at a directory; it serves files with content-type detection, gzip compression, directory listings, and (crucially) SPA-mode rewrites so /profile resolves to index.html for client-side routing.
It's not a production server — there's no clustering, no caching tier, no graceful reload. But for vercel dev-style local previews, demo deployments, and CI smoke tests of build output, it's the path of least friction.
Install
# As a global dev tool (preferred — version not pinned per project)
npm install -g serve
# As a project dev dep (when you want `npx serve` reproducibly)
npm install -D serve
# One-off
npx serve dist
Output: serve binary on PATH. Library import serve (programmatic) and serve-handler (Express/Connect-compatible middleware) also exported.
Versioning & Node support
- Current major line is
14.x(released 2023) — major rewrite for modular handlers, dropped Node 14 support. - Recent releases require Node 18+ or 20+.
- The bundled
serve-handlerpackage (the actual serving logic) is published separately and used by Vercel internals as well.
Package metadata
- Maintainer: Vercel.
- Project home: github.com/vercel/serve
- npm: npmjs.com/package/serve
- License: MIT
- First released: 2014 (as
list, renamed toservein 2017). - Downloads: ~5-7 million per week — partly direct, partly transitive via various CLI scaffolds.
Peer dependencies & extras
serve bundles its own dependencies (chalk, clipboardy, compression, content-disposition, mime, etc.). No peer deps and no companion packages typically needed.
| Adjacent tool | Purpose |
|---|---|
serve-handler | The middleware version — drop into Express/Connect/Polka if you want serve's logic but custom routing |
vercel dev | Vercel's full dev server; if you're targeting Vercel deployment, prefer this over plain serve |
local-web-server | Alternative with more features (HTTPS, proxy, mock endpoints) |
http-server | The other popular static server — see comparison below |
Alternatives
| Tool | Trade-off |
|---|---|
http-server | The older, more bare-bones competitor. Defaults to no SPA fallback; better for pure static (no rewrites). Smaller bundle. |
python -m http.server PORT | Built into Python 3. Zero deps. No SPA fallback, no compression, no auto-MIME beyond a small table. |
bun --bun serve (if Bun installed) | Bun's built-in static server. Faster than serve; require Bun. |
| Caddy | Production-grade static server with auto-HTTPS, HTTP/3, file-server module. Overkill for local but excellent for any "deployed static" use. |
| nginx | The classic. Heavier setup. Use when you outgrow serve in production. |
vite preview | Built into Vite — serves your build output the same way Vite would in dev. Smaller install footprint if you already have Vite. |
Common gotchas
- SPA mode is opt-in.
servedefaults to a404for missing paths. SPAs needserve -s(single-page mode) which rewrites all unmatched requests toindex.html. - The
dist/arg is optional.servewith no arg serves the current directory — useful but dangerous (will expose.env,node_modules, etc., if you forget). - Directory listings default to ON. A path with no
index.htmlshows a navigable directory tree. Disable with-l(no listing) or aserve.jsonconfig. - Default port is 3000. Override with
-p PORTor envPORT=5000. Collisions with other dev servers are common. - HTTPS is NOT built-in. Use
--ssl-cert / --ssl-keywith your own self-signed cert, ormkcertfor trusted local certs. - No
--watch/ file-change reload. For live-reload, use Vite's preview or pair withlive-server. - No proxy support. If you need to proxy
/api/*to a backend during preview, switch tovite preview --host(or a tool likelocal-web-server). - Clipboardy may break in headless CI. On first run,
servetries to copy the URL to clipboard; in containers without xclip/wl-clipboard it logs a warning. SetCLIPBOARD=falseor use--no-clipboard(v14+).
Real-world recipes
Serve a static directory
npx serve dist
Output:
┌──────────────────────────────────────────────────┐
│ Serving! │
│ - Local: http://localhost:3000 │
│ - Network: http://192.168.1.5:3000 │
│ Copied local address to clipboard! │
└──────────────────────────────────────────────────┘
SPA fallback
For any SPA where the router handles client-side paths (React Router, Vue Router, etc.):
npx serve -s dist
Output:
┌──────────────────────────────────────┐
│ Serving! │
│ Local: http://localhost:3000 │
│ SPA mode: 404 → index.html │
└──────────────────────────────────────┘
-s (single-page mode) rewrites any 404 to /index.html, so /users/42 reaches the SPA shell which then routes.
Custom port and host binding
# Pick a non-default port and bind to all interfaces
npx serve -l 5000 dist
# Bind to a specific interface
npx serve -l tcp://127.0.0.1:5000 dist
# Unix socket
npx serve -l unix:/tmp/serve.sock dist
Output:
┌──────────────────────────────────────┐
│ Serving! │
│ Local: http://localhost:5000 │
└──────────────────────────────────────┘
Basic auth via serve.json
serve reads serve.json from the served directory. Auth is implemented in a headers or redirects block (no native basic-auth flag), but the most common usage is serve-handler programmatically:
// server.mjs
import { createServer } from "node:http";
import handler from "serve-handler";
createServer((req, res) => {
const auth = req.headers.authorization;
if (auth !== `Basic ${Buffer.from("admin:hunter2").toString("base64")}`) {
res.writeHead(401, { "WWW-Authenticate": 'Basic realm="preview"' });
return res.end();
}
return handler(req, res, { public: "dist" });
}).listen(3000);
For built-in basic auth without code, use local-web-server or a Caddy reverse proxy.
CORS headers via serve.json
// serve.json (in the served directory)
{
"headers": [
{
"source": "**/*",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Cache-Control", "value": "no-store" }
]
}
]
}
npx serve dist
Output:
┌──────────────────────────────────────┐
│ Serving! │
│ Local: http://localhost:3000 │
└──────────────────────────────────────┘
The headers apply to all responses. source is a glob; use specific patterns for per-route control.
Custom rewrites and redirects
// serve.json
{
"rewrites": [
{ "source": "/api/:slug", "destination": "/api.json" }
],
"redirects": [
{ "source": "/old/:slug", "destination": "/new/:slug", "type": 301 }
]
}
HTTPS for local preview
# Generate a cert with mkcert (https://github.com/FiloSottile/mkcert)
mkcert -install
mkcert localhost
npx serve --ssl-cert localhost.pem --ssl-key localhost-key.pem dist
Output:
The local CA is now installed in the system trust store!
Created a new certificate valid for localhost
┌──────────────────────────────────────┐
│ Serving! │
│ Local: https://localhost:3000 │
└──────────────────────────────────────┘
Output now binds to https://localhost:3000 with a trusted cert.
Smoke-testing a build output
// package.json
{
"scripts": {
"build": "vite build",
"preview": "serve -s dist",
"smoke": "concurrently -k -s first \"npm:preview\" \"wait-on http://localhost:3000 && playwright test\""
}
}
The CI runs npm run build && npm run smoke to confirm the built output works end-to-end.
Production deployment
Don't. serve is not a production server.
For static production hosting:
- Vercel / Netlify / Cloudflare Pages — zero-config CDN-backed static hosting, often free.
- Caddy — single-binary production server with auto-HTTPS.
- nginx — the classic high-performance static server.
- Bun —
bun --bun serveis production-capable. - AWS S3 + CloudFront, Cloudflare R2, etc. — for very-high-volume.
serve lacks: HTTP/2, HTTP/3, automatic TLS, request logging that respects log-rotation, graceful reload, process supervision, ACME integration. The architecture is "single Node process; restart on crash" — fine for previews, not for Cache-Control-aware production.
Performance tuning
serve is a thin layer over Node's http module. Performance knobs are minimal:
--corsadds a CORS header to every response — not a perf issue, but watch large file responses.- Compression is on by default for text mime-types. Disable with
--no-compressionif you're hosting already-gzipped artefacts (e.g. brotli pre-compressed). - No HTTP/2. For static-only with HTTP/2, switch to Caddy or run nginx in front.
For high throughput (>1k req/s), use Caddy or nginx — Node's single-threaded http becomes the bottleneck.
Version migration guide
| From → To | Highlights |
|---|---|
| 12 → 13 | New CLI flags; deprecated --single (use -s). |
| 13 → 14 | Major rewrite. serve-handler extracted as separate package. Node 14 dropped. serve.json schema clarified. |
From legacy alternatives
If you're migrating from python -m http.server, the main extras are: SPA fallback, compression, configurable headers, MIME-type breadth. Direct CLI replacement; same workflow.
Security considerations
- Default directory listings expose secrets. Serving the wrong directory (
.from a project root) leaks.env,node_modules,.git. Always serve a specific subdirectory (dist,public). - No authentication by default. Anyone with network access to the port can fetch every file. Bind to
127.0.0.1for local-only with-l tcp://127.0.0.1:PORT. - Path traversal protection is in
serve-handler. As of recent versions,../requests are rejected. Keep up to date — older versions had path-traversal CVEs. --ssl-cert/--ssl-keyuse whatever you provide. Self-signed certs work; for public exposure use Let's Encrypt via Caddy, notserve.- No rate limiting, no DDoS protection. Putting
servedirectly on the public internet invites trouble.
Configuration patterns
serve.json
Place in the served directory. Full schema includes:
{
"public": "dist",
"cleanUrls": true,
"trailingSlash": false,
"rewrites": [
{ "source": "/app/**", "destination": "/index.html" }
],
"redirects": [
{ "source": "/old/**", "destination": "/new/:_/*", "type": 301 }
],
"headers": [
{
"source": "**/*.{js,css}",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
],
"directoryListing": false,
"renderSingle": true,
"etag": true,
"symlinks": false
}
Programmatic via serve-handler
import { createServer } from "node:http";
import handler from "serve-handler";
createServer((req, res) =>
handler(req, res, { public: "dist", rewrites: [{ source: "**", destination: "/index.html" }] })
).listen(3000);
This is what serve itself does under the hood.
Troubleshooting common errors
EADDRINUSE— port collision.serve -l 5001 distor kill the other process (lsof -i :3000).- "Copied local address to clipboard" warning in CI — clipboardy can't find a clipboard tool. Set
CLIPBOARD=falseenv or use--no-clipboard. - 404s on every SPA route — missing
-sflag. - MIME-type mismatch —
serveuses themimepackage's tables. For non-standard types (.wasm,.jsonvariants), add aheadersblock inserve.jsonsettingContent-Typeexplicitly. - CORS errors — add
headersentry inserve.jsonor use--corsflag (v14+:--corsenables permissive CORS for all routes).
Ecosystem integrations
- Vercel deployments —
serveis what Vercel runs internally for static builds. Local parity is one reason to prefer it overhttp-server. - CI smoke tests — pair with
start-server-and-testorwait-onfor "build → serve → run e2e → tear down" loops. - Docker — minimal Dockerfile:
FROM node:20-alpine; RUN npm install -g serve; COPY dist /app; CMD serve -s -l 3000 /app. Decent for ephemeral previews; not for production.
When NOT to use this
- Production traffic. Use Caddy, nginx, Vercel, Netlify, Cloudflare Pages, or any real CDN.
- You need a reverse proxy / API mocking. Use Vite preview,
local-web-server, or set up a real proxy with Caddy. - Live-reload during development. Use the framework's dev server (Vite, Next, Astro, etc.) — they hot-reload;
servedoesn't. - HTTPS without configuration. Caddy auto-provisions Let's Encrypt;
serverequires manual cert files. - You already have Bun installed.
bun --bun serveis faster and built in. - You want HTTP/2 or HTTP/3. Not supported.
- You only need to share one file briefly.
python -m http.serverorbun --bun serveare smaller-footprint alternatives.
serve is the right tool for the 30-second "verify my SPA build worked" loop. Outside that, reach for a more specialised tool.
See also
- Packages: npm-http-server — alternative static server
- Concept: HTTP — request/response lifecycle