Errors & notFound

Distinguish validation 400s, custom 404s, thrown errors, and how the typed client surfaces them.

Three paths

  1. Schema validation - 400 JSON with details, before the handler; not passed to onError
  2. No matching route - notFound or default 404 JSON
  3. Thrown errors - middleware/handler throws → onError or default 500

Server

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

export const server = new HttpServer()
  .get("/users/:id", (req, res) => {
    if (req.params.id === "boom") throw new Error("Simulated failure");
    return res.json({ id: req.params.id });
  }, [], {
    params: z.object({ id: z.string().min(1) }),
  })
  .notFound((req, res) =>
    res.status(404).json({
      success: false,
      error: "Not Found",
      path: req.url,
      method: req.method,
    }))
  .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,
    }));

export type AppRouter = typeof server;

Client

import type { AppRouter } from "./server.ts";
import { HttpClient, HttpClientError } from "jsr:@webtools/expressapi";

const client = new HttpClient<AppRouter>({ baseUrl: "http://localhost:5050" });

try {
  await client.get("/users/:id", { params: { id: "boom" } });
} catch (error) {
  if (error instanceof HttpClientError) {
    // 500 body from onError
    console.log(error.status, error.body);
  }
}

try {
  await client.get("/missing" as "/users/:id", { params: { id: "x" } });
} catch (error) {
  if (error instanceof HttpClientError) {
    // 404 body from notFound (if the URL were valid at the type level)
  }
}