Advanced usage

Fetch handler, error handling, proxies, and dynamic route registration.

Fetch handler

server.fetch is a standard (Request, info?) => Promise<Response>. Use it with Deno.serve or for portless tests:

Deno.serve({ port: 5050, hostname: "127.0.0.1" }, server.fetch);

Deno.test("GET /users/:id", async () => {
  const response = await server.fetch(new Request("http://localhost/users/42"));
  assertEquals((await response.json()).id, "42");
});

The optional second argument carries the TCP remote address. When absent (as in tests), req.ip falls back to x-forwarded-for if trustProxy is enabled, otherwise null. Full recipe: Testing without a port.

Custom 404

server.notFound((req, res) =>
  res.status(404).json({
    error: "Not Found",
    path: req.url,
    method: req.method,
  })
);

A handler that returns nothing falls back to the default { success: false, error: "404 Not Found." }.

Error handling

Any error thrown by a middleware, a handler, or the validation layer’s unexpected failures is caught and forwarded to the global error handler. Schema validation failures return 400 before the handler and do not go through onError.

server.onError((error, req, res) =>
  res.status(500).json({
    success: false,
    error: "Internal Server Error",
    message: error instanceof Error ? error.message : String(error),
    path: req.url,
  })
);

Returning nothing from onError falls back to { success: false, error: "500 Internal Server Error." }. See Errors & notFound.

Client IP & reverse proxies

req.ip is the remote address of the TCP connection by default, which cannot be spoofed by headers. Behind a trusted reverse proxy, enable trustProxy to read the first entry of x-forwarded-for instead:

const server = new HttpServer({ trustProxy: true });

server.get("/whoami", (req, res) => res.json({ ip: req.ip }));

Dynamic routes

addRoute() registers a route from a plain object, with the same typing as get / post / …: schemas, middleware chain, params inference, and the response shape all feed into HttpClient. It also accepts an optional cors field for per-route CORS.

import { HttpMethods, HttpServer, z } from "jsr:@webtools/expressapi";

const server = new HttpServer()
  .addRoute({
    url: "/users/:id",
    method: HttpMethods.GET,
    requestListener: (req, res) => res.json({ id: req.params.id }),
  })
  .addRoute({
    url: "/users",
    method: HttpMethods.POST,
    requestListener: (req, res) => res.status(201).json({ name: req.body.name }),
    schemas: { body: z.object({ name: z.string() }) },
    cors: { allowOrigin: "https://app.example.com" },
  });