# Build an HTTP service

> Serve HTTP from Elide with a fetch handler or node:http: routing, JSON bodies, streaming, static files, TLS, several contexts and frameworks.

Elide serves HTTP from a fetch handler or from `node:http`; this page builds one service both ways.

## Two ways to serve

| | Fetch handler | `node:http` |
| --- | --- | --- |
| Entry | `export default { fetch }` or `Elide.serve({ fetch })` | `http.createServer(listener).listen(port)` |
| Request and response | Web `Request`, `Response` | `IncomingMessage`, `ServerResponse` |
| HTTPS and HTTP/2 | `cert` and `key` options | Not available |
| `--contexts` | Yes | No; the second copy fails with `EADDRINUSE` |

```ts
// server.ts
export default {
  port: Number(process.env.PORT ?? 3000),
  fetch(request: Request): Response {
    return new Response("hello\n");
  },
};
```

```ts
// node-server.ts
import { createServer } from "node:http";

const port = Number(process.env.PORT ?? 3000);

createServer((req, res) => {
  res.writeHead(200, { "content-type": "text/plain" });
  res.end("hello\n");
}).listen(port, "127.0.0.1");
```

```bash
elide run server.ts
curl -i http://127.0.0.1:3000/
```

```text
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
content-length: 6
server: elide
date: Sat, 10 Oct 2026 00:18:12 GMT

hello
```

`elide run node-server.ts` answers the same request with `Transfer-Encoding: chunked` and `Connection: keep-alive`.

Where each style binds when you give no address:

| Server | Default address |
| --- | --- |
| `export default { fetch }` under `elide run` | `127.0.0.1:8080` |
| `export default { fetch }` under `elide serve` / `elide dev` | `127.0.0.1:8080` / `127.0.0.1:3000`; `--host` and `--port` override |
| `Elide.serve({ port, fetch })` | `0.0.0.0` on `port` |

## Routes, JSON and streaming

`URLPattern`, `Request.json()`, `Response.json()` and `ReadableStream` bodies are globals. This server has an API, a
streamed response and static files from `public/`:

```ts
// app.ts
import { readFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";

const users = new URLPattern({ pathname: "/api/users/:id" });
const types: Record<string, string> = {
  ".html": "text/html; charset=utf-8",
  ".css": "text/css",
  ".js": "text/javascript",
};

async function api(request: Request): Promise<Response | null> {
  const url = new URL(request.url);
  const match = users.exec(url);
  if (match && request.method === "GET") {
    return Response.json({ id: match.pathname.groups.id });
  }
  if (url.pathname === "/api/users" && request.method === "POST") {
    const body = await request.json();
    return Response.json({ created: body.name }, { status: 201 });
  }
  if (url.pathname === "/api/ticks") {
    let n = 0;
    const stream = new ReadableStream({
      async pull(controller) {
        controller.enqueue(new TextEncoder().encode(`tick ${++n}\n`));
        await new Promise((r) => setTimeout(r, 200));
        if (n === 3) controller.close();
      },
    });
    return new Response(stream, { headers: { "content-type": "text/plain" } });
  }
  return null;
}

async function file(pathname: string): Promise<Response> {
  const path = join("public", normalize(pathname === "/" ? "/index.html" : pathname));
  try {
    const body = await readFile(path);
    return new Response(body, { headers: { "content-type": types[extname(path)] ?? "application/octet-stream" } });
  } catch {
    return new Response("not found\n", { status: 404 });
  }
}

export default {
  port: Number(process.env.PORT ?? 3000),
  async fetch(request: Request): Promise<Response> {
    return (await api(request)) ?? file(new URL(request.url).pathname);
  },
};
```

```bash
elide run app.ts
curl http://127.0.0.1:3000/api/users/42
curl -X POST -H 'content-type: application/json' -d '{"name":"ada"}' http://127.0.0.1:3000/api/users
curl -N http://127.0.0.1:3000/api/ticks
```

```text
{"id":"42"}
{"created":"ada"}
tick 1
tick 2
tick 3
```

The POST answers `201 Created`. The three ticks arrive 200 ms apart. `/` returns `public/index.html`, and a missing file
returns `404`.

The same API on `node:http` reads the body with `node:stream/consumers` and streams with `res.write`:

```ts
// node-app.ts
import { createServer } from "node:http";
import { text } from "node:stream/consumers";

createServer(async (req, res) => {
  if (req.method === "POST" && req.url === "/api/users") {
    const body = JSON.parse(await text(req));
    res.writeHead(201, { "content-type": "application/json" });
    res.end(JSON.stringify({ created: body.name }));
    return;
  }
  if (req.url === "/api/ticks") {
    res.writeHead(200, { "content-type": "text/plain" });
    for (let n = 1; n <= 3; n++) {
      res.write(`tick ${n}\n`);
      await new Promise((r) => setTimeout(r, 200));
    }
    res.end();
    return;
  }
  res.writeHead(404).end();
}).listen(Number(process.env.PORT ?? 3000), "127.0.0.1");
```

To serve only files, use `elide serve ./public`; see [Development Server](https://elide.help/docs/dev-server).

## Errors

A fetch handler that throws or rejects answers `500 Internal Server Error` and keeps serving. The error is not printed,
so catch and log it yourself:

```bash
curl -i -d 'not json' http://127.0.0.1:3000/api/users
```

```text
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
content-length: 21
server: elide
date: Sat, 10 Oct 2026 00:34:28 GMT

Internal Server Error
```

## TLS

Pass PEM strings as `cert` and `key` to `Elide.serve`, or as properties of the exported object. The key must be
unencrypted PKCS#8 (`BEGIN PRIVATE KEY`). Clients that offer HTTP/2 get it.

```ts
// tls.ts
import { readFileSync } from "node:fs";

Elide.serve({
  port: Number(process.env.PORT ?? 8443),
  cert: readFileSync("cert.pem", "utf8"),
  key: readFileSync("key.pem", "utf8"),
  fetch(request) {
    return new Response("hello over tls\n");
  },
});
```

```bash
elide run tls.ts
curl -i --cacert cert.pem https://localhost:8443/
```

```text
HTTP/2 200
content-type: text/plain;charset=UTF-8
content-length: 15
server: elide
date: Sat, 10 Oct 2026 00:18:52 GMT

hello over tls
```

`elide serve` and `elide dev` have no TLS flags, and `elide.pkl` has no TLS settings. `https.createServer` throws:

```text
Error: HTTPS server is not supported in this runtime (no host TLS listener); use http.createServer behind a TLS terminator, or https client APIs.
```

## Several contexts

`--contexts=N` runs `N` copies of a fetch-handler app on one port, on Linux. Each copy has its own module state:

```ts
// counter.ts
let count = 0;

export default {
  fetch(): Response {
    count += 1;
    return new Response(`${count}\n`);
  },
};
```

```bash
elide serve --no-tui --contexts=4 counter.ts
for i in 1 2 3 4 5 6 7 8; do curl -s http://127.0.0.1:8080/; done
```

```text
1
1
2
2
3
4
1
2
```

Keep shared state in a database or another service. `--contexts=auto` sizes from the CPU cores; `engine.contexts` in
`elide.pkl` sets it per project ([Engine contexts](https://elide.help/docs/elide-pkl-reference#engine-contexts)).

## Frameworks

Install with `elide install --ecosystems=npm`.

| Framework | Status |
| --- | --- |
| Hono | Works: `export default { fetch: app.fetch }` or `export default app`. |
| Express 5 | Routing, `res.json`, `express.raw()` and reading `req` by hand work. `express.json()` and `express.text()` stop the process with `TypeError: Buffer.isBuffer is not a function`. |
| Fastify 5 | Fails at import: `Error: No such built-in module: node:http2`. |

```ts
// hono-app.ts
import { Hono } from "hono";

const app = new Hono();
app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/users", async (c) => c.json({ created: (await c.req.json()).name }, 201));

export default {
  port: Number(process.env.PORT ?? 3000),
  fetch: app.fetch,
};
```

## Samples

- [HTTP handler](https://elide.help/docs/code-samples#http-handler)
- [Routes with URLPattern](https://elide.help/docs/code-samples#routes-with-urlpattern)
- [Local server and fetch](https://elide.help/docs/code-samples#local-server-and-fetch)
- [Server-sent events](https://elide.help/docs/code-samples#server-sent-events)
- [Request limits](https://elide.help/docs/code-samples#request-limits)
- [Multi-context server](https://elide.help/docs/code-samples#multi-context-server)

## Next steps

- [Deploy an Elide App](https://elide.help/docs/deploy)
- [Elide Global](https://elide.help/docs/api-globals-elide) and [node:http](https://elide.help/docs/api-node-http)
- [Troubleshooting](https://elide.help/docs/troubleshooting)
