File-based routing
The file's location is the URL. api/customers/[id].ts becomes /api/customers/{id}. Nothing to register, nothing to hardcode.
The framework your AI can’t make a mess of. Drop a file, get a typed, documented, validated endpoint.
bun add baguette// api/customers/[id].ts
import { defineRoute, z } from "baguette";
export default defineRoute({
method: "get",
request: { params: z.object({ id: z.string() }) },
response: z.object({ id: z.string(), name: z.string() }),
handler: (c, { params }) =>
c.json({ id: params.id, name: "Ada" }),
});One zod declaration → validation, types, docs, and an error funnel.
Point serve() at a directory. Every file that default-exports a defineRoute is mounted at the path its location implies. That’s the entire wiring step.
// server.ts
import { serve } from "baguette";
serve({ routesDir: "./api" });
// routes loaded · validation on · docs at /api/docsBad input never reaches your handler — 400 automatically.
params, query and body arrive fully typed. No casts.
The spec and the docs page write themselves.
Unhandled throws become a clean 500. One place to look.
The file's location is the URL. api/customers/[id].ts becomes /api/customers/{id}. Nothing to register, nothing to hardcode.
Declare request and response as zod schemas once. Validation, inferred handler types, and the OpenAPI spec all fall out of that single declaration.
A live Scalar UI at /api/docs and the raw spec at /api/doc — generated from your schemas, never hand-wired, never stale.
The framework imports no ORM. Bring Prisma, Drizzle, or raw SQL. Nothing in the hot path you didn't put there.
Drop a file in cron/ for scheduled jobs (Postgres advisory lock) or automations/ for LISTEN/NOTIFY event handlers. Off until the directory exists.
One obvious way to do each thing, enforced. A shipped clean-code contract, a baguette/eslint preset, and baguette check keep the codebase boring and typed.
Generated code drifts because there are a hundred ways to do everything. baguette ships one. A clean-code contract in AGENTS.md, a baguette/eslint preset, and a checker turn those conventions into CI failures — so an agent physically can’t merge the mess.
$ baguette check
api/customers/create.ts
warn hardcoded path — derive it from the file location
warn manual c.req.json() — declare a request.body schema
api/legacy/proxy.ts
error 'as any' in app code — the framework is missing a type
warnings · error · routes cleanbaguette check runs in CI. Clean code passes; clever code doesn’t.