Serving with Elide
You have a web app, an API, or a directory of static files and you need to serve it. Not "eventually, after you write a Dockerfile and configure nginx." Now. elide serve gives you a production-grade HTTP server -- with HTTP/2, HTTP/3, automatic compression, ETags, and an interactive dashboard -- in one command. When you outgrow the defaults, the same command scales to multi-host topologies with reverse proxying, TLS, middleware, and load balancing via a Pkl config file.
Quick start
Serve a build directory
You just ran npm run build and have a dist/ folder. Serve it:
elide serve ./distElide binds to 127.0.0.1:3000, discovers index.html automatically, serves every file with correct MIME types, generates ETags for cache validation, and negotiates Brotli/Zstandard/Gzip compression with the client. Open http://localhost:3000 and you're live.
Run a TypeScript fetch handler
You want dynamic responses. Write a handler:
// api.ts
export default {
fetch(request: Request): Response {
const url = new URL(request.url);
if (url.pathname === "/api/greeting") {
return Response.json({ hello: "world", ts: Date.now() });
}
return new Response("Not Found", { status: 404 });
}
};Then serve it:
elide serve api.tsElide loads your script, calls fetch() for every inbound request, and returns whatever Response you hand back. The contract is the same as Cloudflare Workers and Deno Deploy -- if you've written a fetch handler before, you already know the API.
Use a Pkl config for a full topology
When you need virtual hosts, reverse proxying, middleware, or TLS, reach for a config file:
amends "elide:serve/ElideServer.pkl"
servers {
["app"] {
domains { "myapp.example.com" }
routes {
new {
match { path = "/api/**" }
handler = new ReverseProxy {
upstreams { new { address = "localhost:4000" } }
}
}
new {
handler = new StaticFiles {
root = "./dist"
spaFallback = true
}
}
}
}
} elide serve --config server.pklThis routes /api/** to a backend on port 4000 and serves everything else from ./dist, with SPA fallback so client-side routing works. Validate the config before starting:
elide serve --check-config --config server.pkl---
Static file serving
In directory mode, Elide handles the details you would otherwise configure by hand:
- MIME types -- detected from file extensions, with correct
charset=utf-8for text types - ETags -- content-based, so browsers skip re-downloading unchanged files
- Compression -- Brotli, Zstandard, and Gzip negotiated via
Accept-Encoding. Pre-compressed siblings on disk (e.g.app.js.br) are served directly when present - Range requests -- resumable downloads work out of the box
- Index discovery --
index.htmlis served for directory paths
Dev mode
When you're iterating on a site locally, add --dev:
elide serve --dev ./distThis injects a live-reload script into HTML responses and starts an SSE endpoint at /__elide_dev/livereload. When any file under your root changes, connected browsers reload automatically. Caching is relaxed so you always see the latest version.
SPA fallback
Single-page apps that handle routing client-side need every path to return index.html. In directory mode this requires a Pkl config:
handler = new StaticFiles {
root = "./dist"
spaFallback = true
cacheControl = "public, max-age=300"
}Any request that doesn't match a real file returns index.html instead of 404.
Pre-compression
When preCompress = true, Elide compresses files on first request and caches the result in memory. Subsequent requests skip compression entirely. Combined with on-disk pre-compressed files (app.js.br, app.js.gz), this eliminates per-request compression overhead in production.
Directory listings
For file-browsing use cases, enable autoindex:
handler = new StaticFiles {
root = "./files"
autoindex = true
autoindexShowSizes = true
autoindexShowMtime = true
}Autoindex serves content-negotiated directory listings -- HTML, JSON, or plain text depending on the Accept header.
---
Script mode
Script mode runs a JavaScript or TypeScript file as your server. The file must export a fetch function (or a default export with a fetch method) that receives a Request and returns a Response:
// handler.ts
export default {
fetch(request: Request): Response {
const { method, url } = request;
const { pathname, searchParams } = new URL(url);
if (method === "POST" && pathname === "/api/echo") {
return new Response(request.body, {
headers: { "Content-Type": request.headers.get("Content-Type") ?? "application/octet-stream" }
});
}
return new Response("Method Not Allowed", { status: 405 });
}
}; elide serve handler.tsThe Request and Response objects follow the Fetch API standard. Headers, bodies, status codes, and streaming all work as you'd expect.
Pass arguments to your script after --:
elide serve handler.ts -- --env production --verbose---
Config mode
When you need more than a single directory or script -- virtual hosts, reverse proxying, TLS, middleware stacks, CGI/FastCGI backends -- define a topology in Pkl:
amends "elide:serve/ElideServer.pkl"
tls {
auto = true
acmeEmail = "admin@example.com"
}
servers {
["site"] {
domains { "example.com"; "www.example.com" }
middleware {
new Compress { algorithms { "zstd"; "br"; "gzip" } }
new SecurityHeaders {}
new RateLimit { requests = 100; window = 1.s }
}
routes {
new {
match { host = "www.example.com" }
handler = new Redirect {
target = "https:<<>>
status = 308
}
}
new {
handler = new StaticFiles {
root = " ./dist"
spaFallback = true
preCompress = true
}
}
}
}
}CLI flags (--host, --port, --reactors, --workers, --dev) override the corresponding Pkl values when both are provided, so you can keep a production config and tweak binding locally.
---
Interactive TUI
By default, elide serve launches a terminal dashboard showing:
- Status bar -- bind address, protocol, server state, uptime, and root directory
- Request table -- live stream of every request with method, path, status, and latency
- Metrics panel -- current RPS, active connections, and p50/p95/p99 latency
- RPS sparkline -- 60-second rolling throughput graph
- Log panel -- structured server logs with severity filtering
| Key | Action |
|---|---|
q | Quit the server |
Tab | Switch focus between Requests and Logs panels |
j / k or Arrow keys | Scroll the request table |
PgUp / PgDn | Page through request history |
Esc | Reset focus to the Requests panel |
--no-tui to get plain log output on stderr.
---
CLI reference
elide serve [OPTIONS] [SUBJECT] [-- SCRIPT_ARGS...]Binding
| Flag | Default | Description |
|---|---|---|
—host | 127.0.0.1 | Hostname or IP address to bind to |
—port | 3000 | TCP port to listen on |
SUBJECT | none | Directory (static files), JS/TS file (script mode), or working directory with —config |
— ARGS... | none | Extra arguments forwarded to the script |
Configuration
| Flag | Default | Description |
|---|---|---|
—config, -c | none | Path to a Pkl configuration file |
—check-config | false | Validate config and exit. Alias: —validate |
—dev | false | Enable dev mode: live reload, script injection, relaxed caching |
—no-tui | false | Disable the interactive TUI; use plain log output |
Performance
| Flag | Default | Description |
|---|---|---|
—reactors | 1 | Number of reactor (event loop) threads |
—workers | 0 (auto) | Total worker threads across all reactors. 0 auto-detects from available CPUs |
—workers-per-reactor | 0 | Workers per reactor. Takes precedence over —workers |
—pin-reactors | false | Pin each reactor to a CPU core (Linux only) |
—recv-pool-max | auto | Maximum recv buffers per size class per reactor pool |
Admin API
| Flag | Default | Description |
|---|---|---|
—admin-port | none | Enable admin API on this TCP port |
—admin-host | 127.0.0.1 | Bind address for the admin API |
—admin-socket | none | Admin API on a Unix domain socket (mutually exclusive with —admin-port) |
—admin-token | none | Bearer token for admin API authentication |
---
What's next
- elide expose -- Put your local server on the internet with automatic TLS and a public URL
- elide share -- Serve a directory and expose it in one step
- Serve Configuration Reference -- Complete Pkl schema for handlers, middleware, TLS, listeners, and the admin API
- CLI Reference -- All Elide commands and global options