Skip to content

Introduction

Tetsu is an HTTP framework for Bun. Controllers get their dependencies as function arguments, hooks run in fixed slots instead of a middleware chain, and the compiler infers every type from the path to the handler. There are no decorators, no DI container and no dependencies in the core.

import { controller, createApp, httpError, route } from "@tetsujs/core";
import { z } from "zod";
const users = controller("Users", ({
repo: UserRepository
repo
}: {
repo: UserRepository
repo
: UserRepository }) => ({
get: 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: "/users/:id"

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
: "/users/:id",
schema: { params: z.object({ id: z.coerce.number() }) },
handler: (ctx: {
readonly out: Outgoing;
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {
id: number;
};
}) => User

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
: (
ctx: {
readonly out: Outgoing;
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {
id: number;
};
}
ctx
) => {
const
const user: User | undefined
user
=
repo: UserRepository
repo
.find(
ctx: {
readonly out: Outgoing;
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {
id: number;
};
}
ctx
.
params: {
id: number;
}
params
.id);
id: number
if (!
const user: User | undefined
user
) throw httpError(404, "USER_NOT_FOUND");
return
const user: User
user
;
},
}),
}));
Bun.serve({ ...createApp({ routes: users({
repo: UserRepository
repo
}) }) });

Nothing here is annotated. ctx.params.id is a number because the schema converts the path segment, and without :id in the path it would not exist. A request with a bad id gets a 422 and never reaches the handler.

Tetsu runs on Bun only. It has no plugin system: a package such as CORS or rate limiting is a hook, mounted like your own. It has no container, no file-based routing and no global registry: what runs for a route is declared in code, on the route, its groups or the application.