Getting Started
Get up and running with @adi-family/http in 5 minutes.
Installation
Install the core library and your preferred adapter:
bash
# Core library
npm install @adi-family/http zod
# Choose an adapter
npm install @adi-family/http-express # For Express
# or
npm install @adi-family/http-native # For Native Node.jsbash
# Core library
bun add @adi-family/http zod
# Choose an adapter
bun add @adi-family/http-express # For Express
# or
bun add @adi-family/http-native # For Native Node.jsProject Structure
We recommend organizing your project like this:
your-project/
├── packages/
│ ├── api-contracts/ # Shared API contracts
│ │ ├── users.ts
│ │ └── projects.ts
│ ├── backend/ # Server implementation
│ │ └── handlers/
│ │ ├── users.ts
│ │ └── projects.ts
│ └── client/ # Client application
│ └── api/
│ └── users.tsQuick Start
1. Define Your First Route
Create a contract that both client and server will use:
typescript
// packages/api-contracts/users.ts
import { route } from '@adi-family/http'
import { z } from 'zod'
import type { HandlerConfig } from '@adi-family/http'
export const getUserConfig = {
method: 'GET',
route: route.dynamic(
'/api/users/:id',
z.object({ id: z.string() })
),
response: {
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email()
})
}
} as const satisfies HandlerConfig2. Implement the Server Handler
typescript
// packages/backend/handlers/users.ts
import { handler } from '@adi-family/http'
import { getUserConfig } from '@api-contracts/users'
export const getUserHandler = handler(getUserConfig, async (ctx) => {
// ctx.params is fully typed!
const user = await db.users.findById(ctx.params.id)
if (!user) {
throw new Error('User not found')
}
return user // Response is type-checked!
})3. Serve with Express
typescript
// packages/backend/index.ts
import express from 'express'
import { serveExpress } from '@adi-family/http-express'
import { getUserHandler } from './handlers/users'
const app = express()
app.use(express.json())
serveExpress(app, [getUserHandler])
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})Or with Native HTTP:
typescript
// packages/backend/index.ts
import { serveNative } from '@adi-family/http-native'
import { getUserHandler } from './handlers/users'
const server = serveNative([getUserHandler], {
port: 3000,
hostname: '0.0.0.0',
onListen: (port, hostname) => {
console.log(`Server running on http://${hostname}:${port}`)
}
})4. Use in Client
typescript
// packages/client/api/users.ts
import { BaseClient } from '@adi-family/http'
import { getUserConfig } from '@api-contracts/users'
const client = new BaseClient({
baseUrl: 'http://localhost:3000',
headers: {
Authorization: `Bearer ${token}`
}
})
// Fully type-safe!
const user = await client.run(getUserConfig, {
params: { id: '123' }
})
console.log(user.name) // ✅ TypeScript knows this exists
console.log(user.age) // ❌ TypeScript error - property doesn't existNext Steps
Now that you have a basic setup:
- Route Builder - Learn about the powerful route builder API
- Handlers - Deep dive into handler functions
- Validation - Understand Zod validation
- Examples - See more practical examples
Common Patterns
POST with Body
typescript
export const createUserConfig = {
method: 'POST',
route: route.static('/api/users'),
body: {
schema: z.object({
name: z.string().min(1),
email: z.string().email()
})
},
response: {
schema: z.object({ id: z.string() })
}
} as const satisfies HandlerConfigGET with Query Parameters
typescript
export const listUsersConfig = {
method: 'GET',
route: route.static('/api/users'),
query: {
schema: z.object({
page: z.number().optional(),
limit: z.number().optional(),
search: z.string().optional()
})
},
response: {
schema: z.array(z.object({
id: z.string(),
name: z.string()
}))
}
} as const satisfies HandlerConfigMultiple URL Parameters
typescript
export const getTaskConfig = {
method: 'GET',
route: route.dynamic(
'/api/projects/:projectId/tasks/:taskId',
z.object({
projectId: z.string(),
taskId: z.string()
})
),
response: {
schema: z.object({
id: z.string(),
title: z.string()
})
}
} as const satisfies HandlerConfigTips
Always use as const satisfies HandlerConfig
This ensures proper type inference while maintaining type safety.
Share contracts between client and server
Export configs from a shared @api-contracts package to keep client and server in sync.
Don't validate URL params
URL params are only used for building paths. Validate query and body instead.
Use Zod for validation
Zod provides excellent TypeScript integration and runtime validation.