Typed client

HttpClient is typed by the server type with no runtime dependency on the server.

Setup

import type is enough - the server module is never evaluated, so nothing starts and no port opens.

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

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

const user = await client.get("/users/:id", { params: { id: "42" } });
//    ^ { id: string; name: string }

const created = await client.post("/users", { body: { name: "Charlie" } });

URLs, methods, params, query, body, and response shapes are checked. input is optional when the route needs none of them. headers may always be passed and is merged over the client’s default headers.

await client.get("/nope"); // ❌ unknown URL
await client.post("/users/:id", { params: { id: "1" } }); // ❌ no POST on this URL
await client.get("/users/:id"); // ❌ params are required
await client.get("/users/:id", { params: { id: 1 } }); // ❌ id must be a string

Errors

Any response outside the 2xx range throws an HttpClientError carrying the parsed body:

try {
  await client.get("/users/:id", { params: { id: "42" } });
} catch (error) {
  if (error instanceof HttpClientError) {
    console.error(error.status); // 404
    console.error(error.body);   // parsed response body
    console.error(error.url);    // full requested URL
  }
}

Empty and non-JSON bodies

  • Empty responses (204) yield null
  • Non-JSON responses are returned as text rather than throwing

Options

new HttpClient<AppRouter>({
  baseUrl: "http://localhost:5050", // trailing slashes trimmed
  headers: { authorization: "Bearer token" },
  fetch: customFetch, // alternative fetch, useful in tests
});

REST + client recipe: REST API.