Serverless
Bun
Serve an @hulla/api contract directly from Bun with a Web-standard request handler.
Use @hulla/api/fetch to serve an @hulla/api implementation from Bun’s native HTTP server. Bun owns the listener;
the Fetch adapter owns contract matching, declared input and response handling, and the Request/Response boundary.
This guide assumes a Bun project with a contract and server implementation to expose. It finishes with a local
GET /api/profiles/:id request and a JSON response.
Install the packages
The example uses Zod for its request and response schemas. No Bun-specific @hulla/api package is required.
bun add @hulla/api zodnpm install @hulla/api zodpnpm add @hulla/api zodyarn add @hulla/api zodDeclare and implement one route
Keep the contract in code shared by any clients and the server implementation in server-only code:
// src/api/contract.ts
import { defineContract, response, route } from '@hulla/api'
import { z } from 'zod'
export const contract = defineContract({
basePath: '/api',
routes: {
profile: route.get('/profiles/:id', {
params: z.object({ id: z.string() }),
responses: {
200: response.json(
z.object({
id: z.string(),
runtime: z.literal('bun'),
})
),
},
}),
},
})// src/api/server.ts
import { defineServer } from '@hulla/api/server'
import { contract } from './contract'
export const implementation = defineServer(contract).implement({
profile: ({ params, response }) =>
response(200, {
id: params.id,
runtime: 'bun',
}),
})The contract owns the /api/profiles/:id path and validates id before the handler runs. The implementation returns
the status and body declared by that route.
Mount the handler in Bun
Create the Bun entrypoint at src/index.ts. Bun.serve receives the mounted Fetch handler directly:
// src/index.ts
import { fetchAdapter } from '@hulla/api/fetch'
import { implementation } from './api/server'
const handler = fetchAdapter().mount(implementation)
const server = Bun.serve({
hostname: '127.0.0.1',
port: Number(Bun.env['PORT'] ?? 3000),
fetch: handler,
})
console.log(`Listening on ${server.url}`)Start the server from the project root:
bun run src/index.tsIn another terminal, call the route through Bun’s HTTP listener:
curl --include http://127.0.0.1:3000/api/profiles/user-1The response proves both boundaries selected the route: Bun delivered the request to handler, and the adapter
matched the contract’s /api base path and profile route.
HTTP/1.1 200 OK
content-type: application/json
{"id":"user-1","runtime":"bun"}Keep Bun and the adapter responsibilities separate
Bun decides how the server listens and what happens before or after the Fetch handler. Configure its hostname, port,
TLS, logging, authentication, and platform limits in the Bun application. fetchAdapter() does not add a default body
limit; set its maxBodyBytes option when the application needs an adapter-owned byte budget, while still respecting
any limits imposed by Bun or an upstream proxy.
The handler uses Web Request and Response values. The adapter matches the request’s full pathname, so a request to
/profiles/user-1 does not match this contract even though Bun delivered it to the listener. If the contract declares
a body or a stream, the Fetch adapter reads and writes those Web representations; Bun remains responsible for the
underlying connection and listener lifecycle.
For adapter options and the client-side Fetch transport, read the Fetch transport guide. For request-scoped values and middleware, read server context.
Use Bun’s HTTP server documentation for listener options and timeout behavior. Those server settings can end a connection independently of the adapter, including a stream that stays idle longer than the configured server timeout.