Serverless
Azure Functions
Register your API as an Azure Functions v4 HTTP trigger.
On this page
@hulla/api-azure-functions turns your implementation into an Azure HTTP handler. Register it with app.http() in an Azure Functions Node.js v4 project.
This guide assumes an Azure Functions v4 project already exists. The example uses Zod for its request and response schemas.
bun add @hulla/api @hulla/api-azure-functions @azure/functions zodnpm install @hulla/api @hulla/api-azure-functions @azure/functions zodpnpm add @hulla/api @hulla/api-azure-functions @azure/functions zodyarn add @hulla/api @hulla/api-azure-functions @azure/functions zodDeclare the HTTP path shared by server and client code. The contract includes /api because Azure’s default route
prefix remains in the URL received by the handler:
// 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 and register the mounted handler. app.http() returns void, so export the handler itself when
another module needs to invoke the adapter boundary directly:
// src/functions/api.ts
import { app } from '@azure/functions'
import { azureFunctionsAdapter } from '@hulla/api-azure-functions'
import { defineServer } from '@hulla/api/server'
import { contract } from '../api/contract'
const implementation = defineServer(contract).implement({
profile: ({ params, response }) =>
response(200, {
id: params.id,
runtime: 'azure-functions',
}),
})
export const handler = azureFunctionsAdapter().mount(implementation)
app.http('api', {
authLevel: 'anonymous',
methods: ['GET'],
route: '{*path}',
handler,
})The catch-all trigger lets one function serve every route in the contract. List every HTTP method used by the contract
in methods; Azure rejects an unlisted method before the request reaches @hulla/api.
Invoke the exported handler with Azure’s own request and invocation types to verify the integration without deploying:
// scripts/verify-azure.ts
import { HttpRequest, InvocationContext } from '@azure/functions'
import { handler } from '../src/functions/api'
const result = await handler(
new HttpRequest({
method: 'GET',
url: 'https://example.azurewebsites.net/api/profiles/user-1',
}),
new InvocationContext({
functionName: 'api',
invocationId: 'local-1',
})
)
console.log(result)
// {
// status: 200,
// headers: { 'content-type': 'application/json' },
// jsonBody: { id: 'user-1', runtime: 'azure-functions' }
// }The handler returns HttpResponseInit | HttpResponse, not a Web Response. Azure turns that native value into the
public HTTP response.
For each invocation:
- The Azure host selects a function using its route, allowed methods, and authorization settings.
- Azure calls the registered handler with
HttpRequestandInvocationContextvalues. - The adapter reads the method and full URL pathname, then matches a route in the contract.
- It decodes and validates the declared query, headers, and body before running context, middleware, and the route handler.
- It validates the declared result and translates it to
HttpResponseInit | HttpResponse.
An unmatched contract path returns 404; a known path with the wrong method returns 405 only when Azure’s trigger
allows that method through. Trigger routing and authorization failures happen before the adapter runs.
Azure prefixes HTTP trigger routes with api by default. That prefix is present in HttpRequest.url, so it must also
be present in the path matched by the contract.
host.json route prefix | Trigger route | Public path | Matching contract path |
|---|---|---|---|
api (default) | {*path} | /api/profiles/user-1 | /api/profiles/:id |
"" | {*path} | /profiles/user-1 | /profiles/:id |
Configure a different prefix with extensions.http.routePrefix in host.json, then make the contract basePath
match the pathname Azure passes to the handler. Microsoft documents this host setting in Customize an HTTP
endpoint.
Do not add the host prefix twice. With the default Azure prefix and a contract
basePathof/api, the client callshttps://example.azurewebsites.net/api/...; it does not add another/apioutside the contract path.
Use adapter.context() when handlers need Azure’s invocation ID, logger, authenticated user, or another value from the
native request. Define the context and mount the implementation through the same adapter:
// src/functions/api.ts
import { app } from '@azure/functions'
import { azureFunctionsAdapter } from '@hulla/api-azure-functions'
import { defineServer } from '@hulla/api/server'
import { contract } from '../api/contract'
const adapter = azureFunctionsAdapter()
const implementation = defineServer(contract, {
context: adapter.context(({ request, invocationContext, route }) => ({
invocationId: invocationContext.invocationId,
user: request.user,
routeKey: route.key.join('.'),
})),
}).implement({
profile: ({ params, context, response }) => {
console.info(context.invocationId, context.routeKey, context.user)
return response(200, {
id: params.id,
runtime: 'azure-functions',
})
},
})
export const handler = adapter.mount(implementation)
app.http('api', {
authLevel: 'anonymous',
methods: ['GET'],
route: '{*path}',
handler,
})An adapter-native context binds the implementation to the Azure Functions adapter. Mounting it through another adapter fails during setup rather than supplying incompatible native state.
This adapter does not receive a client-disconnect signal from the Azure handler. The portable signal therefore does
not abort when the caller disconnects; application work that needs cancellation must use an Azure-appropriate policy.
The adapter reads request bodies through the methods on Azure’s HttpRequest. The declaration’s content type selects
the corresponding representation before validation.
| Contract representation | Azure Functions adapter behavior |
|---|---|
| Empty | Returns status and headers without a body. |
| JSON | Reads request.json() and returns JSON with jsonBody. |
| Text | Reads request.text() and returns a string in body. |
| Bytes | Reads request.arrayBuffer() and returns Uint8Array in body. |
| Form data | Reads request.formData() and returns FormData in body. |
| Stream response | Returns AsyncIterable<Uint8Array> in body without collecting its chunks in the adapter. |
| Raw response | Passes through an Azure-native response only when its status matches the declared contract status. |
There is no stream or raw request-body representation, and the adapter does not expose a body-size option. Azure host and platform limits apply before or around the handler.
Returning an async iterable is only one half of HTTP streaming. Enable streaming before registering functions when the deployed host must send chunks incrementally:
import { app } from '@azure/functions'
app.setup({ enableHttpStream: true })Azure HTTP streaming requires the Node.js v4 programming model and compatible Functions host, Core Tools, and
@azure/functions versions. Check Microsoft’s current HTTP streaming requirements
before relying on incremental delivery. A failure produced while a stream is being consumed happens after the handler
has returned and is not handled by the mount-level onError hook.
Pass onError to mount() to record failures with both the request and invocation state:
// src/functions/api.ts
export const handler = adapter.mount(implementation, {
onError({ error, phase, request, invocationContext, defaultResponse }) {
invocationContext.error({
error,
phase,
requestUrl: request.url,
invocationId: invocationContext.invocationId,
})
return defaultResponse
},
})The hook receives the protocol-safe default Azure response. Return it or undefined to keep the fallback, or return
another HttpResponseInit | HttpResponse with a numeric status to replace it. If the hook throws or returns an
invalid replacement, the adapter keeps the default response without invoking the hook again.
Use declared contract responses for expected application outcomes. onError is for failures while matching routes,
decoding or validating inputs, creating context, running middleware or handlers, validating outputs, or translating the
Azure response.
Timer, queue, blob, Event Hub, and other triggers are application events rather than @hulla/api HTTP requests. Call
the underlying application service directly from those functions instead of passing their payloads to this adapter.
The adapter returns an HTTP handler. It does not create the function app, select a Node.js runtime, register the
trigger, configure authorization or bindings, change host.json, or deploy infrastructure.
- 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 function through the same contract.
- Adapter requirements compares body ownership, streaming, routing, and cancellation across hosts.