Knead

Search every guide and package page, down to section headings.

↑↓ move Enter open Esc close

A TypeScript framework for Bun 1.4.3+

Structured servers for Bun, secure from the first request.

NestJS-style modules, inject() dependency injection and decorated controllers on standard TC39 decorators. Routes compile to Bun's native router, and request limits, credential redaction and production error masking are already on.

bun add @knead-js/core @knead-js/http
TypeScript
import { inject } from '@knead-js/core';
import { Controller, type Ctx, Delete, Get, Post } from '@knead-js/http';
import * as v from 'valibot';
import { TodosService } from './todos.service.js';

const TodoId = v.object({
  id: v.pipe(v.string(), v.digits(), v.transform(Number)),
});

const CreateTodo = v.object({
  title: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(200)),
});

@Controller('/todos')
export class TodosController {
  readonly #todos = inject(TodosService);

  @Get('/')
  list() {
    return this.#todos.list();
  }

  @Get('/:id', { params: TodoId })
  get({ params }: Ctx<{ params: v.InferOutput<typeof TodoId> }>) {
    return this.#todos.get(params.id);
  }

  @Post('/', { body: CreateTodo, status: 201 })
  create({ body }: Ctx<{ body: v.InferOutput<typeof CreateTodo> }>) {
    return this.#todos.create(body.title);
  }

  @Delete('/:id', { params: TodoId })
  remove({ params }: Ctx<{ params: v.InferOutput<typeof TodoId> }>) {
    this.#todos.remove(params.id);
  }
}

The getting started app. Every example on this site is compiled against the packages, and its output is pinned by tests.

01 / The request pipeline

Every request takes the same path.

Each layer has one job and a fixed place, so you always know what ran before your handler and what sees the error when something throws.

  1. 01 Host check A Host outside allowedHosts gets 421 before anything runs.
  2. 02 Middleware Global by priority, then module, controller and route.
  3. 03 Guards Pass only on exactly true. Anything else is a 403.
  4. 04 Validation params, query, headers and body; every issue in one 400.
  5. 05 Authorize Guards that see the validated, transformed input.
  6. 06 Handler Its return value becomes the response.
On throw

The error goes to the route's exception filters, then the controller's, then the global ones, and finally becomes an RFC 9457 problem response. It never escapes as an exception.

How the pipeline works

02 / Defaults

The careful choices are already made.

Secure before you configure anything

Limits that are easy to forget are on from the start. Raise one deliberately.

Request body
1 MiB413
Query parameters
1000400
WebSocket message
256 KiBclosed
Sockets per IP
100429
GraphQL depth / complexity
10 / 1000rejected
Errors outside development
maskedcorrelation id
The security model

Standard decorators

TC39 decorators as TypeScript and Bun ship them. No experimentalDecorators, no reflect-metadata.

@Controller('/todos')
export class TodosController {
  readonly #todos = inject(TodosService);
}

Typed by your schemas

Any Standard Schema library validates params, query, headers and body. The handler gets the schema's output, already transformed.

Validation

Tests run against a real server

Test.createModule() compiles your modules with overrides, and createApp() starts the full pipeline on a random port on 127.0.0.1.

Testing

One JSON line per event, credentials removed

{"time":"2026-10-17T09:30:00.000Z","level":"error","logger":"knead","msg":"Unhandled error on GET /reports (correlation id 0f5e9c1a-…): Error: replica postgres://***@db/shop is down"}
Logging

03 / Packages

Thirteen packages, one version.

Every package is published under @knead-js and released together. Add only the ones you use.