API Endpoints

A +server.ts file is a plain HTTP endpoint (no page is rendered). Export one function per method it handles:


// routes/api/posts/+server.ts
import { created, ok } from "@torpor/build";
import { type ServerEndPoint } from "@torpor/build";
import { createPost, getPosts } from "@/lib/db";

export default {
	get: async () => ok(await getPosts()),
	post: async ({ json }) => {
		const body = await json();
		const post = await createPost(body);
		return created(post);
	},
} satisfies ServerEndPoint;

The handler names are get, post, patch, put, del (DELETE), options and head.

The request event

Handlers receive an event with json() and form() to read the body, query() to read the query string, params for the route's dynamic segments, plus request, cookies, headers and appData.

Validation

Declare a schema per handler (any Standard Schema library works) to validate and type the request before the handler runs. A body or query string that fails gets a 422 response; params that fail get a 404, since the URL can't refer to an existing resource:


const schema = {
	params: z.object({ id: z.coerce.number() }),
	get: z.object({ sort: z.enum(["asc", "desc"]) }),
	post: z.object({ title: z.string() }),
};

export default {
	schema,
	get: async ({ query }) => ok(await query()),
	post: async ({ json }) => created(await createPost(await json())),
} satisfies ServerEndPoint<"/api/posts/[id]", typeof schema>;

Typed clients

Call an endpoint from client code with makeApi. Params are filled in from the route path, JSON responses are parsed, and the result is typed from the endpoint's responses. Failed (non-2xx) requests throw:


import type endpoint from "@/routes/api/posts/[id]/+server";

const post = makeApi<"/api/posts/[id]", typeof endpoint>("/api/posts/[id]", { id: 5 });
const data = await post.get();

OpenAPI

Handler schemas are converted to JSON Schema, so a full OpenAPI document can be generated for the site. Run tb --openapi to write it to openapi.json (or pass a file path).