Skip to content

createApp

createApp() compiles the routes, groups and hooks into a route table and returns an application in the shape Bun.serve takes.

const
const app: App<RoutesOf<object[]>>
app
= createApp({
routes: object[]

The topology: a group, a controller, or an array of either.

routes
,
hooks: { afterResponse: [log] },
cookies?: CookieOptions | undefined

How cookies are signed, when they are.

Configuration rather than schema: a secret is not a shape, so it has no place in a route's schema.cookies, and the policy is the application's rather than any one endpoint's. A covered cookie is sealed on the way out and verified on the way in without a call site mentioning it, which is the point — a signature nobody can forget to apply.

cookies
: {
secret: string

The key the signature is derived from — 32 random bytes or more.

Only ever used through HMAC-SHA256; it is not an encryption key, and a signed cookie's value is still readable by the client. Signing answers "did this value come from us", not "can this be seen".

An empty or missing secret is refused at startup: with it, anyone can compute the signature, and a forged cookie would read as ours.

secret
: Bun.
const env: Bun.Env & NodeJS.ProcessEnv & ImportMetaEnv

The environment variables of the process

Defaults to process.env as it was when the current Bun process launched.

Changes to process.env at runtime won't automatically be reflected in the default value. For that, you can pass process.env explicitly.

env
.COOKIE_SECRET!,
sign?: string | true | readonly string[] | undefined

Which cookies are signed. true covers every one of them.

A list is the safer default of the two: sealing everything means a cookie set by anything other than this application — an analytics script, a proxy — fails verification and reads as absent, which is a confusing way to discover the setting.

sign
: ["session"] },
maxBodySize?: number | undefined

Maximum size in bytes of a request body the framework reads: a route's with a body schema, a bodyType or rawBody. Defaults to 1 MiB.

A content-length above the limit is rejected with a 413 before a single byte is read; a chunked request is dropped as soon as the buffered stream crosses the limit. Either way an oversized body costs no parsing and no validation. A "stream" body is the exception: it is counted as the handler reads it, so its 413 comes with the handler already running, and none comes if the handler stops reading first.

The chunked rejection abandons the stream mid-flight, which leaves the connection's framing broken: the client gets its 413, but that connection is spent and the next request over it fails. Declared bodies are read to the end and cost the connection nothing.

The framework only guards the body it parses itself: a handler reading ctx.req directly is not capped. The ceiling for everything else is Bun's own maxRequestBodySize (default 128 MiB), set where the app is served: Bun.serve({ ...app, maxRequestBodySize }).

maxBodySize
: 2 * 1024 * 1024,
reportError: ({
source: FailureSource
source
,
error: unknown

What was thrown, exactly as it was thrown: not formatted, not truncated, so a logger's redaction sees the fields it knows.

error
}) => console.error(
source: FailureSource
source
,
error: unknown

What was thrown, exactly as it was thrown: not formatted, not truncated, so a logger's redaction sees the fields it knows.

error
),
});
Bun.serve({ ...
const app: App<RoutesOf<object[]>>
app
,
port?: string | number | undefined

The port the server listens on

@default ― process.env.PORT || "3000"

port
: 3000 });
Option Type Default
routes object | readonly object[] required a controller, a group, a route, a ws() endpoint, or an array of them
hooks hooks keyed by slot none application hooks, run for every request
cookies CookieOptions none the secret and the names of signed cookies
maxBodySize number 1048576 (1 MiB) the largest request body, in bytes
validateResponses boolean true check results against schema.response
validation { status?: 400 | 422 } { status: 422 } the status of a validation failure
fallback (ctx: BaseCtx) => unknown the 404 envelope answers a path no route matches
reportError (report: FailureReport) => unknown print to console.error receives failures no response can carry

Arrays nest, and a controller is read by its own string-keyed fields. See Groups and mounting.

An object keyed by slot: { beforeParse: [a, b], afterResponse: [c] }. Application hooks run before group and route hooks in every slot, except onError, where they run last. They are the only hooks that run for a 404, a 405 and an OPTIONS request. See Hook slots.

Field Type Default
secret string required the HMAC-SHA256 key, 32 random bytes or more
sign true | string | readonly string[] true the names to sign; true signs every cookie

An empty or missing secret throws a TypeError at startup. See Cookies.

A content-length above the limit is a 413 before any byte is read, and a body without one is cut at the chunk that crosses the limit. A "stream" body is counted as the handler reads it: crossing the limit errors the stream with the same 413, with the handler already running, and a handler that stops reading first gets none. A route’s own maxBodySize overrides this one. A handler that reads ctx.req itself is not capped.

When true, a serialized result is checked against the schema of its status. A status the response map does not declare, a body under a bodiless status, a value or nothing under a status whose contentType is not JSON, and response headers or cookies that fail their schema are refused too. Each failure is a 500 and a report with source: "response". A Response built by the handler is never checked.

false turns every response check off. The framework never reads NODE_ENV: to check outside production only, pass Bun.env.NODE_ENV !== "production".

422 by default. Set 400 if your API uses 400 for every broken contract. A malformed JSON or form body is a 400 either way.

Replaces the 404 for unmatched paths, and runs with the application’s hooks. Its result is serialized like a handler’s: 200, or 204 for undefined, unless it sets ctx.out.status or returns a Response. An SPA index wants the 200; a custom 404 must set its status.

Receives { source, error, ctx } for each failure no response can carry: an error no onError hook answered, a broken response contract, a failing onError or afterResponse hook, a WebSocket handler, a stream, a shutdown step. ctx is typed from the application’s own hooks, each field optional, and is absent when there was no request. It is called in place and never awaited. See Errors.

Field Type
routes Record<string, PathHandler> one native Bun route per declared path
fetch (req, server) => Promise<Response> the no-match handler: the 404 or the fallback
websocket WebSocketHandler<SocketState> the handler for every socket; always present
maxRequestBodySize number, optional Bun’s own body cap, raised above the largest maxBodySize; set only when that exceeds Bun’s default of 128 MiB
entries readonly RouteTableEntry[] the compiled route table, for tooling
options AppOptions the options in force: validationStatus, validateResponses, maxBodySize, and cookieSealer when cookies is set
printRoutes () => void prints each route’s method, full path and controller

Bun.serve({ ...app }) takes routes, fetch, websocket and maxRequestBodySize and ignores the rest. Bun’s own options go after the spread:

const
const app: App<RoutesOf<object[]>>
app
= createApp({
routes: object[]

The topology: a group, a controller, or an array of either.

routes
});
Bun.serve({
...
const app: App<RoutesOf<object[]>>
app
,
port?: string | number | undefined

The port the server listens on

@default ― process.env.PORT || "3000"

port
: 3000,
idleTimeout?: number | undefined

Sets the number of seconds to wait before timing out a connection due to inactivity.

@default ― 10

idleTimeout
: 30,
websocket: Bun.WebSocketHandler<unknown>

Enable websockets with

Bun.serve

Upgrade a

Request

to a

ServerWebSocket

with

Server.upgrade

Pass data in

Server.upgrade

to attach data to the

ServerWebSocket.data

property

websocket
: { ...
const app: App<RoutesOf<object[]>>
app
.
websocket: Bun.WebSocketHandler<SocketState>

The WebSocket handler of every socket endpoint in the table.

Always present, even when nothing declares a socket: Bun.serve only lets a route hand a connection over when the server has one, and an application whose type depended on whether it happens to contain a socket endpoint would be a worse trade than an inert handler.

Bun's server-level options — idleTimeout, maxPayloadLength, backpressureLimit — are not proxied, for the same reason tls is not: spread what you need over it.

Bun.serve({ ...app, websocket: { ...app.websocket, idleTimeout: 30 } });

websocket
,
idleTimeout?: number | undefined

Sets the number of seconds to wait before timing out a connection due to no messages or pings.

@default ― 120

idleTimeout
: 60 },
});

app.fetch does not route: Bun’s router does, and fetch runs only when no path matched. Calling it directly always takes the fallback, so tests go through a real server with serve() from @tetsujs/core/testing.

Each path answers HEAD from its GET route, OPTIONS with 204 and an Allow header, and any other undeclared method with 405 and the same Allow.

app.entries[n] has method, path (prefixes joined), def, hooks (the merged chains), route (what ctx.route is), and controller, name and ws when they apply.

AppRoutes<typeof app> is the type of every HTTP route, keyed by "METHOD /full/path", with its schemas. A typed client is built from it. Socket endpoints are not in it.

createApp throws when:

  • two routes have the same method and full path;
  • two paths differ only in parameter names (/users/:id, /users/:userId);
  • two controllers share a name;
  • the same hook instance is mounted twice in one route’s chain;
  • a class is mounted instead of an instance, or an application as a child;
  • a controller declares a route under a symbol key or as a class getter;
  • hooks is a list, a key is not a slot, a slot is not a list, an element is not a hook, or a hook sits in a slot other than its own;
  • cookies.secret is empty or missing.

A controller without routes is a warning on console.warn.

Once the table is built, each mounted controller with an [onMount](app) method receives the application, once, before createApp returns.

Export Kind Reference
createApp function this page
route function route
controller function route
group function route
ws function route
hook object of factories Hook slots
onMount symbol the key of a controller method [onMount](app), called with the built application
isRoute, isGroup, isWs functions type guards for a RouteDef, a GroupNode, a WsDef
HttpError, httpError, errorBody, ValidationError classes and functions Framework error codes
ResponseContractError, reportFailure class, function Framework error codes
signedCookie function Context fields
toJsonSchema function toJsonSchema(schema, { target }, direction?): a schema’s JSON Schema, or undefined when the validator cannot produce one

@tetsujs/core/testing exports testCtx(parts, { cookies }), serve(app, { hostname, stop }), stopServers() and captureErrors(). See Testing.

Area Types Reference
Application AppConfig, App, AppOptions, AppContext, AppRoutes, FallbackHandler, PathHandler, RoutedRequest this page
Route table RouteMap, RouteSignature, RoutesOf, RouteTableEntry, Mountable this page
Routes RouteConfig, RouteDef, RouteDocs, Method, SchemaConfig, ResponseEntry, HandlerResult, HandlerMustReturn, HandlerReturnMarker, ValidateResult, ResultError route
Paths ExtractParams, ValidatePath, ValidatePrefix, PathError route
Groups GroupNode, GroupOptions, GroupConfig, GroupHooks route
WebSockets WsConfig, WsDef, WsSchemaConfig, Socket, SocketData, SocketState, MessageOf route
Hooks Hook, AnyHook, SlotName, SlotBases, SentResponse, HooksConfig, MergedHooks, HookSlotError, HookRequirementError, HookStackError Hook slots
Context BaseCtx, EarlyCtx, ValidatedCtx, HandlerCtx, ResponseCtx, ErrorCtx, Requires, RouteInfo, Outgoing, DeclaredOutgoing, DeclaredStatus, BodyType, ParsedBody, FormBody, FormValue Context fields
Cookies CookieOptions, CookieAttributes, ResponseCookies cookies above, Context fields
Errors ErrorBody, ValidationIssue, FailureReport, FailureSource, ReportError Framework error codes
Schemas AnySchema, InferInput, InferOutput, JsonSchemaDirection, Standard* the Standard Schema interfaces