Express Adapter
The Express adapter (@adi-family/http-express) allows you to serve type-safe handlers using Express.js.
Installation
bash
npm install @adi-family/http @adi-family/http-express express zodbash
bun add @adi-family/http @adi-family/http-express express zodBasic Usage
typescript
import express from 'express'
import { serveExpress } from '@adi-family/http-express'
import { getUserHandler, createUserHandler } from './handlers'
const app = express()
// Required: Parse JSON bodies
app.use(express.json())
// Register your handlers
serveExpress(app, [
getUserHandler,
createUserHandler
])
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})How It Works
The serveExpress function:
- Registers routes - Maps each handler's route config to Express routes
- Parses requests - Extracts params, query, and body
- Validates data - Validates query and body using Zod schemas
- Executes handlers - Calls your handler functions
- Returns responses - Sends JSON responses with proper status codes
- Handles errors - Catches and formats validation and runtime errors
Complete Example
1. Define Contracts
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 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(),
email: z.string()
}))
}
} as const satisfies HandlerConfig
export const getUserConfig = {
method: 'GET',
route: route.pattern('/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 HandlerConfig
export const createUserConfig = {
method: 'POST',
route: route.static('/api/users'),
body: {
schema: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(18).optional()
})
},
response: {
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string()
})
}
} as const satisfies HandlerConfig
export const updateUserConfig = {
method: 'PATCH',
route: route.pattern('/api/users/:id', z.object({ id: z.string() })),
body: {
schema: z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional()
})
},
response: {
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string()
})
}
} as const satisfies HandlerConfig
export const deleteUserConfig = {
method: 'DELETE',
route: route.pattern('/api/users/:id', z.object({ id: z.string() })),
response: {
schema: z.object({ success: z.boolean() })
}
} as const satisfies HandlerConfig2. Implement Handlers
typescript
// packages/backend/handlers/users.ts
import { handler } from '@adi-family/http'
import {
listUsersConfig,
getUserConfig,
createUserConfig,
updateUserConfig,
deleteUserConfig
} from '@api-contracts/users'
import * as db from '../db/users'
export const listUsersHandler = handler(listUsersConfig, async (ctx) => {
const { page = 1, limit = 10, search } = ctx.query
return await db.findAll({ page, limit, search })
})
export const getUserHandler = handler(getUserConfig, async (ctx) => {
const user = await db.findById(ctx.params.id)
if (!user) {
throw new Error('User not found')
}
return user
})
export const createUserHandler = handler(createUserConfig, async (ctx) => {
const user = await db.create(ctx.body)
return user
})
export const updateUserHandler = handler(updateUserConfig, async (ctx) => {
const user = await db.update(ctx.params.id, ctx.body)
if (!user) {
throw new Error('User not found')
}
return user
})
export const deleteUserHandler = handler(deleteUserConfig, async (ctx) => {
await db.remove(ctx.params.id)
return { success: true }
})3. Set Up Express Server
typescript
// packages/backend/index.ts
import express from 'express'
import { serveExpress } from '@adi-family/http-express'
import * as userHandlers from './handlers/users'
const app = express()
// Middleware
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// Register handlers
serveExpress(app, [
userHandlers.listUsersHandler,
userHandlers.getUserHandler,
userHandlers.createUserHandler,
userHandlers.updateUserHandler,
userHandlers.deleteUserHandler
])
// Start server
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`)
})Using with Express Middleware
You can use Express middleware alongside your handlers:
typescript
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import morgan from 'morgan'
import { serveExpress } from '@adi-family/http-express'
const app = express()
// Global middleware
app.use(cors())
app.use(helmet())
app.use(morgan('combined'))
app.use(express.json())
// Health check route (regular Express)
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
// Type-safe API routes
serveExpress(app, [
getUserHandler,
createUserHandler
])
// 404 handler (regular Express)
app.use((req, res) => {
res.status(404).json({ error: 'Not found' })
})
// Error handler (regular Express)
app.use((err, req, res, next) => {
console.error(err)
res.status(500).json({ error: 'Internal server error' })
})
app.listen(3000)Route Registration
The adapter automatically registers routes based on your handler configs:
typescript
// This handler config:
const config = {
method: 'GET',
route: route.pattern('/api/users/:id', z.object({ id: z.string() }))
}
// Creates this Express route:
app.get('/api/users/:id', handler)Supported HTTP Methods
GETPOSTPUTPATCHDELETE
Request Processing
URL Parameters
Extracted from the URL pattern:
typescript
const config = {
route: route.pattern(
'/api/projects/:projectId/tasks/:taskId',
z.object({ projectId: z.string(), taskId: z.string() })
)
}
// In handler:
handler(config, async (ctx) => {
ctx.params.projectId // From URL
ctx.params.taskId // From URL
})Query Parameters
Parsed from query string and validated:
typescript
const config = {
query: {
schema: z.object({
page: z.number().optional(),
limit: z.number().optional()
})
}
}
// Request: GET /api/users?page=2&limit=20
handler(config, async (ctx) => {
ctx.query.page // 2 (number)
ctx.query.limit // 20 (number)
})Numbers are automatically converted from strings.
Request Body
Parsed from JSON and validated:
typescript
const config = {
body: {
schema: z.object({
name: z.string(),
email: z.string().email()
})
}
}
// Request: POST /api/users with JSON body
handler(config, async (ctx) => {
ctx.body.name // Validated string
ctx.body.email // Validated email
})Headers
Access request headers:
typescript
handler(config, async (ctx) => {
const auth = ctx.headers.get('Authorization')
const contentType = ctx.headers.get('Content-Type')
})Response Handling
Success Responses
Return data from your handler:
typescript
handler(config, async (ctx) => {
return { id: '123', name: 'Alice' }
})
// Automatically sends: 200 OK with JSON bodyStatus Codes
The adapter automatically sets:
200- Successful GET, PUT, PATCH, DELETE201- Successful POST400- Validation errors404- Route not found500- Server errors
Error Handling
Validation Errors
Automatic Zod validation:
typescript
// Request with invalid data
POST /api/users
{
"name": "", // Too short
"email": "invalid" // Not an email
}
// Automatic response:
{
"error": "Validation failed",
"details": [
{ "path": ["name"], "message": "String must contain at least 1 character(s)" },
{ "path": ["email"], "message": "Invalid email" }
]
}Runtime Errors
Throw errors in handlers:
typescript
handler(config, async (ctx) => {
const user = await db.findById(ctx.params.id)
if (!user) {
throw new Error('User not found')
}
return user
})
// Sends 500 with error messageFor custom error handling, see Error Handling.
Advanced Configuration
Custom Error Responses
typescript
import { serveExpress } from '@adi-family/http-express'
const app = express()
app.use(express.json())
serveExpress(app, handlers)
// Add custom error handler after serveExpress
app.use((err, req, res, next) => {
if (err.statusCode === 404) {
res.status(404).json({ error: 'Resource not found' })
} else if (err.name === 'ValidationError') {
res.status(400).json({ error: err.message, details: err.details })
} else {
res.status(500).json({ error: 'Internal server error' })
}
})Mounting at Prefix
typescript
const app = express()
const apiRouter = express.Router()
// Mount all handlers under /api
serveExpress(apiRouter, handlers)
app.use('/api', apiRouter)
// Routes are now at /api/users, /api/users/:id, etc.Multiple Handler Groups
typescript
import { serveExpress } from '@adi-family/http-express'
const app = express()
app.use(express.json())
// Register different handler groups
serveExpress(app, userHandlers)
serveExpress(app, projectHandlers)
serveExpress(app, taskHandlers)Performance Tips
- Use express.json() middleware - Required for parsing request bodies
- Enable compression - Use
compressionmiddleware for large responses - Use caching - Add caching middleware for frequently accessed data
- Connection pooling - Use database connection pools
typescript
import compression from 'compression'
import { serveExpress } from '@adi-family/http-express'
const app = express()
app.use(compression())
app.use(express.json())
serveExpress(app, handlers)API Reference
serveExpress(app, handlers)
Registers handlers with an Express app.
Parameters:
app: Express | Router- Express app or router instancehandlers: Handler[]- Array of handlers to register
Returns: void
Example:
typescript
import express from 'express'
import { serveExpress } from '@adi-family/http-express'
const app = express()
serveExpress(app, [getUserHandler, createUserHandler])Next Steps
- Middleware - Add custom middleware to handlers
- Error Handling - Custom error handling strategies
- Testing - Test your Express routes
- Examples - See complete examples