Start here
@hulla/api
Type-check HTTP requests, handlers, and clients against one shared contract, across the hosts and transports your app uses.
On this page
@hulla/api keeps methods, paths, inputs, and response types in one shared contract. Your server implements that contract; your client calls it through the transport you choose.
import { defineContract, request, response, route } from '@hulla/api'
import { createClient } from '@hulla/api/client'
import { inProcessTransport } from '@hulla/api/in-process'
import { defineServer } from '@hulla/api/server'
const contract = defineContract({
routes: {
createProject: route.post('/projects', {
body: request.json<{ name: string }>(),
responses: { 201: response.json<{ id: string; name: string }>() },
}),
},
})
const implementation = defineServer(contract).implement({
createProject: ({ body, response }) =>
response(201, { id: crypto.randomUUID(), name: body.name }),
})
const api = createClient(contract, {
transport: inProcessTransport(implementation),
})
const result = await api.createProject({ body: { name: 'Documentation' } })| Concern | How it works |
|---|---|
| HTTP behavior | Methods, paths, headers, and statuses stay explicit. A declared 404 is a typed result the client can handle. |
| Framework choice | Native adapters connect the implementation to your host. The host keeps routing, middleware, and deployment. |
| Runtime validation | Add a Standard Schema validator where needed. Type-only declarations skip application-shape validation. |
| Contract drift | Missing handlers, undeclared statuses, and incorrect response types become TypeScript errors. |
Use codecs for values such as Date that need different in-memory and wire representations.
These tools overlap, but they start from different sources of truth:
| Tool | The source of truth | It feels most natural when |
|---|---|---|
@hulla/api | A standalone HTTP contract | HTTP details matter and the same contract must cross different hosts or transports |
| tRPC | A server procedure router | One TypeScript application wants inference directly from its server implementation |
| oRPC | Procedures or a separate contract | You want its RPC/OpenAPI builders and broad procedure ecosystem |
| ts-rest | A standalone REST contract | You want a contract-first REST client with its supported server integrations |
| Hono RPC | A typed Hono route chain | The server already belongs to Hono and clients should infer from that app |
Choose @hulla/api when the boundary should outlive any one framework and when transport choice should remain explicit. If the API is only an internal function call, use an ordinary function. If a framework-owned typed client already matches every consumer, adding a separate contract may not buy you enough.
The core package includes contracts, clients, servers, Fetch, and in-process calls. Add integrations only for the boundary you need.