Build typesafe JSON REST APIs and clients using Mastro and TypeScript.
The client is a thin wrapper around fetch.
Validate input on the server by bringing your own Standard Schema-compliant validation library (e.g. Zod, Valibot or validate.js).
No bundler required, as only types are shared between the client and server.
Place the following into routes/todo/[id].server.ts (this is using a vendored validate.js).
import { Err, Ok } from "@mastrojs/result";
import { boolean, object, optional, string } from "../../validate.js";
export type TodoPatch = typeof PATCH;
export const { PATCH } = jsonRoute({
method: "PATCH",
path: `/todo/${"id" as string}`,
params: object({ id: string }),
body: object({ done: optional(boolean), title: optional(string) }),
}, async ({ body, params }) => {
const { done, title } = body;
const { id } = params;
const updatedTodo = await db.updateTodo(id, { done, title });
return updatedTodo ? Ok(updatedTodo) : Err("Not found", 404);
});For example in routes/todo-list.client.ts:
import { fetchApi } from "@mastrojs/api/client";
import type { TodoPatch } from "./todo/[id].server.ts";
// onUpdate:
const res = await fetchApi<TodoPatch>("PATCH", `/todo/${todo.id}`, todo);deno add jsr:@mastrojs/api
pnpm add jsr:@mastrojs/api
bunx jsr add @mastrojs/api
For a full working example, see todo-list-typesafe-api.
To see all functions @mastrojs/api exports, see its API docs.
@mastrojs/api is not the first library to use shared types to safely integrate the frontend with the backend, but it is likely the smallest. The source code is as straightforward as possible, using no type-level magic except a phantom type to carry over the type information over from the frontend to the backend.
Unlike tRPC, @mastrojs/api allows you to use REST – i.e. the full HTTP semantics, including all possible HTTP verbs (not only POST).
Compared to Hono RPC or Elysia, typechecking is fast – no matter how many routes you have. This comes at the cost of you having to type out the path parameter manually again.