REST API + typed client

CRUD routes, export the server type, and call it from another module without codegen.

Prerequisites

  • deno add jsr:@webtools/expressapi
  • Keep the server importable for types - guard listen() with import.meta.main

Server

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

export const server = new HttpServer()
  .get("/users", (_req, res) =>
    res.json({ users: [] as { id: string; name: string }[] }))
  .get("/users/:id", (req, res) => {
    const user = findUser(req.params.id);
    if (!user) return res.json({ found: false as const });
    return res.json({ found: true as const, name: user.name });
  })
  .post("/users", (req, res) =>
    res.status(201).json({ id: "1", name: req.body.name }), [], {
      body: z.object({
        name: z.string().min(3),
        email: z.string().email(),
      }),
    })
  .put("/users/:id", (req, res) =>
    res.json({ updated: true, id: req.params.id }))
  .delete("/users/:id", (_req, res) => res.status(204).send(null));

export type AppRouter = typeof server;

if (import.meta.main) server.listen(5050);

Client

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

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

const users = await client.get("/users");
const user = await client.get("/users/:id", { params: { id: "1" } });
if (user.found) console.log(user.name);

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

await client.delete("/users/:id", { params: { id: "1" } }); // null

Related: Typed client, Routing.