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:

bash
 elide serve ./dist

Elide 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:

typescript
// 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:

bash
 elide serve api.ts

Elide 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:

pkl
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
        }
      }
    }
  }
}
bash
 elide serve --config server.pkl

This 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:

bash
 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-8 for 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.html is served for directory paths

Dev mode

When you're iterating on a site locally, add --dev:

bash
 elide serve --dev ./dist

This 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:

pkl
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:

pkl
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:

typescript
// 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 });
  }
};
bash
 elide serve handler.ts

The 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 --:

bash
 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:

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.

The Pkl config schema supports six handler types (StaticFiles, ReverseProxy, Cgi, FastCgi, Redirect, Respond) and over a dozen middleware (compression, rate limiting, CORS, JWT auth, basic auth, IP filtering, and more). See the Serve Configuration Reference for the complete schema.

---

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
KeyAction
qQuit the server
TabSwitch focus between Requests and Logs panels
j / k or Arrow keysScroll the request table
PgUp / PgDnPage through request history
EscReset focus to the Requests panel
For CI, containers, or log aggregators, pass --no-tui to get plain log output on stderr.

---

CLI reference

elide serve [OPTIONS] [SUBJECT] [-- SCRIPT_ARGS...]

Binding

FlagDefaultDescription
—host127.0.0.1Hostname or IP address to bind to
—port3000TCP port to listen on
SUBJECTnoneDirectory (static files), JS/TS file (script mode), or working directory with —config
— ARGS...noneExtra arguments forwarded to the script

Configuration

FlagDefaultDescription
—config, -cnonePath to a Pkl configuration file
—check-configfalseValidate config and exit. Alias: —validate
—devfalseEnable dev mode: live reload, script injection, relaxed caching
—no-tuifalseDisable the interactive TUI; use plain log output

Performance

FlagDefaultDescription
—reactors1Number of reactor (event loop) threads
—workers0 (auto)Total worker threads across all reactors. 0 auto-detects from available CPUs
—workers-per-reactor0Workers per reactor. Takes precedence over —workers
—pin-reactorsfalsePin each reactor to a CPU core (Linux only)
—recv-pool-maxautoMaximum recv buffers per size class per reactor pool

Admin API

FlagDefaultDescription
—admin-portnoneEnable admin API on this TCP port
—admin-host127.0.0.1Bind address for the admin API
—admin-socketnoneAdmin API on a Unix domain socket (mutually exclusive with —admin-port)
—admin-tokennoneBearer token for admin API authentication
The admin API exposes health checks, Prometheus metrics, a live HTMX dashboard, cache management, and graceful config reload. See the Serve Configuration Reference for the full endpoint list.

---

What's next