createApp() compiles the routes, groups and hooks into a route table and
returns an application in the shape Bun.serve takes.
const
constapp: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.
constenv: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.
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.
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".
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.
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
constapp:App<RoutesOf<object[]>>
app=createApp({
routes: object[]
The topology: a group, a controller, or an array of either.
routes });
Bun.serve({
...
constapp: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: { ...
constapp: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.
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.