Start here
Migrate from @hulla/api 1.x
Migrate a 1.x procedure to a working 2.0 API route.
On this page
Move one route at a time from the 1.x procedure API to a 2.0 contract, server handler, and client call. This example starts with the stable 1.2 API and ends with a working request.
If your app already uses defineContract(), defineServer(), and createClient(), it uses the current model.
In 1.x, the procedure carried its output and implementation together. In 2.x, the shared contract stops before server behavior begins.
-import { api } from '@hulla/api'
-import { z } from 'zod'
-
-const a = api()
-export const health = a
- .procedure('health', 'get')
- .output(z.object({ ok: z.literal(true) }))
- .define(() => ({ ok: true }))
+import { defineContract, response, route } from '@hulla/api'
+
+export const contract = defineContract({
+ routes: {
+ health: route.get('/health', {
+ responses: { 200: response.json<{ ok: true }>() },
+ }),
+ },
+})The contract is intentionally incomplete on its own. That is what makes it safe to share.
Create a server-only module and cover every selected route:
import { defineServer } from '@hulla/api/server'
import { contract } from './contract'
export const implementation = defineServer(contract).implement({
health: ({ response }) => response(200, { ok: true }),
})Direct handler returns and implicit success statuses are gone. response(200, body) connects the runtime value to the exact contract branch it fulfills.
The portable implementation does not start a server. Mount it in the host that already owns routing and deployment.
import { fetchAdapter } from '@hulla/api/fetch'
import { implementation } from './implementation'
export const handler = fetchAdapter().mount(implementation)handler is a (Request) => Promise<Response> function. Use a native adapter instead when Express, Fastify, Hono, or another framework should keep its own router and context.
The v2 client starts from a contract node and an explicit transport. There is no generated runtime and no terminal builder step.
import { createClient } from '@hulla/api/client'
import { fetchTransport } from '@hulla/api/fetch'
import { contract } from './contract'
const health = createClient(contract.routes.health, {
transport: fetchTransport({ baseUrl: 'https://api.example.com' }),
})
const result = await health()
if (result.status === 200) {
console.log(result.body.ok) // true
}Build a client from the root contract for the whole call tree, or from a router/route selection when a module needs only part of it.
- Positional input becomes named HTTP input. Pass
{ params, query, headers, body }so the call mirrors the declared boundary. - Expected failures become status values. Declare a
404or409, then narrowresult.statuson the client. - Client middleware moves to construction. Pass
middleware: [...]tocreateClient(). - Framework state stays at the adapter. Expose only the request context handlers actually need.
- Local calls use a transport. Reach the same implementation with
inProcessTransport()when caller and server share a process.
The compiler will catch missing routes and wrong response shapes. It cannot decide your boundary policy.
Before finishing the migration, review:
- whether each JSON declaration should be type-only, schema-validated, or a codec;
- which non-
2xxoutcomes are expected contract responses; - request-body limits and preservation required by the host;
- repeated response headers, stream ownership, and cancellation;
- browser/server module boundaries for clients and implementations.
The migration reference is the old-to-new lookup for package moves and removed 1.x behavior. For the new model in context, continue with API contracts, Implementing the server, and Calling the API.