Serverless
Cloudflare Workers
Serve your API from a Cloudflare Worker and use its bindings.
On this page
@hulla/api-cloudflare connects your API to a Worker’s fetch() entrypoint. Mount the implementation there to handle HTTP requests and access bindings such as KV from your handlers.
This guide assumes the Worker project and its Wrangler configuration already exist. The example uses Zod for request and response schemas.
bun add @hulla/api @hulla/api-cloudflare zodnpm install @hulla/api @hulla/api-cloudflare zodpnpm add @hulla/api @hulla/api-cloudflare zodyarn add @hulla/api @hulla/api-cloudflare zodDeclare the HTTP boundary in a module shared by server and client 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.string() })),
},
}),
},
})Implement the route in server-only code:
// 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: 'cloudflare-worker',
}),
})Mount the implementation as the Module Worker’s fetch handler:
// src/index.ts
import { cloudflareAdapter } from '@hulla/api-cloudflare'
import { implementation } from './api/server'
export default {
fetch: cloudflareAdapter().mount(implementation),
}The mounted function has the same (request, env, ctx) shape that Cloudflare calls. Verify the adapter boundary
without deploying or opening a port:
// scripts/verify-worker.ts
import worker from '../src/index'
const result = await worker.fetch(
new Request('https://worker.example/api/profiles/user%201'),
{},
{
waitUntil() {},
passThroughOnException() {},
}
)
console.log(result.status) // 200
console.log(await result.json())
// { id: 'user 1', runtime: 'cloudflare-worker' }The call uses an ordinary Web Request and receives an ordinary Web Response. In production, Cloudflare supplies
the real bindings and execution context to that same mounted function.
For each HTTP request:
- Cloudflare calls the exported
fetchhandler withrequest,env, andctx. - The adapter matches the method and pathname against the selected contract routes.
- It decodes and validates declared path parameters, query values, headers, and any request body.
@hulla/apicreates server context, runs server middleware, and calls the handler.- The adapter returns the declared result as a Web
Response.
An unmatched path returns 404; a known path with the wrong method returns 405 with an Allow header. Mounting a
server fragment limits matching to that fragment’s routes. The adapter does not add Worker
routes or Wrangler configuration around the mounted handler.
Run wrangler types in the Worker project when application context needs generated binding and runtime types:
wrangler typesPass the generated Env type to cloudflareAdapter(), then use adapter.context() to expose only the native values
needed by handlers:
// src/index.ts
import { defineServer } from '@hulla/api/server'
import { cloudflareAdapter } from '@hulla/api-cloudflare'
import { contract } from './api/contract'
type Env = {
SERVICE_NAME: string
}
const adapter = cloudflareAdapter<Env>()
const implementation = defineServer(contract, {
context: adapter.context(({ env, ctx, request, route }) => ({
defer: ctx.waitUntil.bind(ctx),
requestId: request.headers.get('cf-ray'),
routeKey: route.key.join('.'),
serviceName: env.SERVICE_NAME,
})),
}).implement({
profile: ({ params, context, response }) => {
console.info(context.requestId, context.routeKey)
return response(200, {
id: params.id,
runtime: context.serviceName,
})
},
})
export default {
fetch: adapter.mount(implementation),
}The adapter’s built-in execution-context type includes waitUntil() and passThroughOnException(). When code needs
additional properties from Wrangler’s generated ExecutionContext, pass that type as the second generic argument:
const adapter = cloudflareAdapter<Env, ExecutionContext>()adapter.context() binds the implementation to the cloudflare-workers adapter. Mounting it through Pages, generic
Fetch, or the in-process transport fails during setup rather than supplying incompatible native state.
The adapter reads a contract-declared body from the Web Request before creating context or calling the handler. Read
the decoded value from the handler’s typed body field. For a body route, the native request visible to a context
factory has already been consumed.
The Cloudflare adapter does not expose preserveRequestBody or maxBodyBytes. If outer Worker code also needs the
body, clone the request before passing the original to the mounted handler. Cloudflare’s request limits still apply at
the platform boundary.
Use onError to observe or replace failures while the adapter decodes inputs, creates context, runs server middleware
or the handler, serializes a response, or converts it to a Web Response:
const adapter = cloudflareAdapter<Env>({
onError({ error, phase, request, env, ctx, defaultResponse }) {
console.error(phase, request.url, env.SERVICE_NAME, ctx, error)
return defaultResponse
},
})The hook receives the invocation’s env and ctx plus a clone of the protocol-safe default Response. Return another
Response to replace it, or return undefined to keep the default. Options passed to mount() override adapter
defaults; onError: undefined disables an inherited hook for that mount.
Declared streams become Web ReadableStream bodies. Cancelling the response stream calls the source iterator’s
return() method, and a producer failure is reported with phase: 'transport'. Once the response has been returned,
the hook can observe a late stream failure but cannot replace bytes already in flight. The handler’s signal is the
incoming Request.signal; pass it to work that supports cooperative cancellation.
The package root targets the Module Worker fetch(request, env, ctx) handler. It does not implement legacy Service
Worker event syntax or Cloudflare’s scheduled, queue, email, alarm, or Durable Object lifecycles. Those entrypoints do
not represent an @hulla/api HTTP request and should call application services directly.
D1, KV, R2, service bindings, and Durable Object namespaces need no adapter-specific API. Configure them in the
Worker project, include them in Env, and expose the values handlers need through server context.
- Server context and middleware explains portable request context and route policies.
- Server fragments and composition shows how to mount selected route groups.
- Calling the API explains how application code calls the deployed Worker through the same contract.
- Cloudflare’s fetch-handler documentation covers bindings, execution context, and deployment behavior.