Fundamentals

Implementing the server

Write handlers for your contract and call the finished server.

On this page

defineServer(contract).implement(...) connects your contract to the code that handles each route. The contract checks the handler inputs and the responses you return.

Here, you’ll implement the contract’s three routes in src/api/server.ts, then call one to check the response.

Import the existing contract and pass a complete handler tree to implement(). The tree follows the contract’s keys; it does not repeat URL paths.

src/api/server.ts
import { defineServer } from '@hulla/api/server'
import { contract } from './contract'

export const server = defineServer(contract)

export const implementation = server.implement({
  health: ({ response }) => response(200, 'ok'),
  users: {
    byId: ({ params, response }) =>
      params.id === 'ada'
        ? response(200, { id: 'ada', name: 'Ada' })
        : response(404, { message: 'User not found' }),
    rename: async ({ params, body, response }) =>
      response(200, { id: params.id, name: body.name }),
  },
})

The two highlighted calls separate the public contract from its implementation. defineServer(contract) prepares the typed server builder; implement() checks this selected handler tree against it.

The contract parses and validates declared request inputs before calling a handler.

Handler inputWhat it contains
params, query, headers, bodyDecoded application values, present only when the route declares them
contextThe object returned by the server’s context factory
signalA portable AbortSignal for cancellable work
routeThe matched route’s literal key, method, and full path
errorsDeclared error factories, when the contract defines errors
response(status, body?, headers?)A result checked against that route’s declared status, body, and headers

The selected status determines which response body and headers TypeScript accepts. A handler may return only the statuses it needs, while clients still see every status declared by the contract.

Here, byId reads its validated path parameter and returns one of its declared 200 or 404 responses. rename receives a validated JSON body. The tree is complete because it implements both children of users as well as health.

fetchAdapter().mount() adapts the implementation to the Web Request and Response boundary.

TypeScript
// src/api/http.ts
import { fetchAdapter } from '@hulla/api/fetch'
import { implementation } from './server'

export const fetch = fetchAdapter().mount(implementation)

const result = await fetch(new Request('http://localhost/api/users/ada'))
console.log(result.status) // 200
console.log(await result.json()) // { id: 'ada', name: 'Ada' }

The mounted handler matches GET /api/users/:id, runs byId, and serializes the declared response. In an application, export the handler in the form expected by the host framework. Choose a host integration in server adapters. To call the same implementation inside one process, use the client and inProcessTransport() from the quick start.

Next, read server context and middleware for request-scoped values and policies, server fragments and composition for modular or independently mountable implementations, or error handling for declared application failures.