Skip to content

Installation

The core is the only package an application needs:

Terminal window
bun add @tetsujs/core
Bun 1.4 or later
TypeScript 5.7 or later, including 7, with strict on
Bun’s types @types/bun
moduleResolution bundler, node16 or nodenext

A project made with bun init already has Bun’s types and a tsconfig.json that works. For an existing project, add Bun’s types:

Terminal window
bun add -d @types/bun

and check tsconfig.json against this one:

{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "bundler",
"types": ["bun"],
"strict": true,
"noEmit": true,
"skipLibCheck": true
}
}
  • strict is required. Without strictNullChecks the types cannot tell a field that exists from one that might not.
  • The old node resolution (also called node10) does not read package exports and reports @tetsujs/core as not found.
  • Stricter flags such as noUncheckedIndexedAccess and exactOptionalPropertyTypes work: the packages are checked under @tsconfig/strictest.

Bun’s types also describe what Bun adds to the platform’s globals. Their fetch has preconnect, so typeof fetch requires it too, and a stand-in typed with it, such as a fake in a test, fails to compile with “Property ‘preconnect’ is missing”. Type a dependency on fetch by its call instead. fetch itself still fits:

type Fetch = (
input: string | URL | Request
input
: string | URL | Request,
init: RequestInit | undefined
init
?: RequestInit) => Promise<Response>;
const weatherController = controller("Weather", ({
fetch: Fetch
fetch
}: {
fetch: Fetch
fetch
: Fetch }) => ({
today: route({
method: "GET"

The method this route answers.

Kept as a literal rather than widened to

Method

: the method is half of a route's identity, and an application that remembers its routes — for a generated client, for tooling — needs to know which one this is.

method
: "GET",
path: "/weather"

Route path with :param segments, e.g. "/orders/:id/cancel".

Must start with /, contain no empty segments and no trailing slash; a malformed literal is a compile error.

path
: "/weather",
handler: (ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}) => Promise<any>

The endpoint logic; ctx is fully inferred, never annotate it.

The return type is inferred rather than demanded, and checked twice over. Against the route's own contract, by the

HandlerResult

bound on R: answering with something response never declared is a compile error that says so, instead of a structural diff against Response. And against what the framework can serialize at all, by the intersected

ValidateResult

: a stream handed over bare is refused whether or not the route declared anything, because that is the case no contract covers — without a response schema HandlerResult is unknown and accepts every value there is.

handler
: async () => (await
fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>
fetch
("https://api.example.com/today")).json(),
}),
}));
weatherController({
fetch: Fetch
fetch
}); // the real one
weatherController({
fetch: Fetch
fetch
: async () => Response.json({
sunny: boolean
sunny
: true }) }); // a fake, in a test

Tetsu validates through Standard Schema, which Zod, Valibot and ArkType implement. The core depends on none of them; install the one you use:

Terminal window
bun add zod

TypeBox needs the adapter @tetsujs/typebox.

Everything else is optional. All packages share one version number and are released together.

Package What it adds
@tetsujs/openapi an OpenAPI 3.1 document and a docs page, generated from the routes
@tetsujs/typebox TypeBox schemas as DTOs, file uploads included
@tetsujs/cors CORS
@tetsujs/rate-limit rate limiting with a replaceable store
@tetsujs/request-id request ids
@tetsujs/request-log access and arrival logs
@tetsujs/secure-headers security headers
@tetsujs/sse server-sent events and streamed responses
@tetsujs/static static files: a built site, a single-page app or assets
@tetsujs/lifecycle graceful shutdown

Next: Quick start.