📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-05-29 08:33:53 +00:00
parent fdb52f1e96
commit 06e0d13d57
1615 changed files with 232858 additions and 0 deletions
@@ -0,0 +1,744 @@
# Error Handling
Centralized error handling with `HTTPException` and `onError`.
## HTTPException
Throw typed HTTP errors with status codes and optional metadata.
### Basic Usage
```typescript
import { HTTPException } from 'hono/http-exception';
app.get('/users/:id', (c) => {
const user = findUser(c.req.param('id'));
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ user });
});
```
### With Cause
```typescript
app.get('/users/:id', (c) => {
const id = c.req.param('id');
const user = findUser(id);
if (!user) {
throw new HTTPException(404, {
message: 'User not found',
cause: { userId: id, timestamp: Date.now() }
});
}
return c.json({ user });
});
```
### Status Codes
```typescript
// 400 Bad Request
throw new HTTPException(400, { message: 'Invalid request' });
// 401 Unauthorized
throw new HTTPException(401, { message: 'Missing or invalid token' });
// 403 Forbidden
throw new HTTPException(403, { message: 'Insufficient permissions' });
// 404 Not Found
throw new HTTPException(404, { message: 'Resource not found' });
// 409 Conflict
throw new HTTPException(409, { message: 'Email already registered' });
// 422 Unprocessable Entity
throw new HTTPException(422, { message: 'Validation failed' });
// 429 Too Many Requests
throw new HTTPException(429, { message: 'Rate limit exceeded' });
// 500 Internal Server Error
throw new HTTPException(500, { message: 'Internal server error' });
// 503 Service Unavailable
throw new HTTPException(503, { message: 'Service temporarily unavailable' });
```
## Custom Error Classes
Extend `HTTPException` for domain-specific errors.
### Common Error Classes
```typescript
import { HTTPException } from 'hono/http-exception';
export class ValidationError extends HTTPException {
constructor(message: string, issues?: Record<string, string>) {
super(400, {
message,
cause: issues,
});
}
}
export class UnauthorizedError extends HTTPException {
constructor(message = 'Unauthorized') {
super(401, { message });
}
}
export class ForbiddenError extends HTTPException {
constructor(message = 'Forbidden') {
super(403, { message });
}
}
export class NotFoundError extends HTTPException {
constructor(resource: string, id?: string) {
super(404, {
message: `${resource} not found`,
cause: id ? { [resource.toLowerCase() + 'Id']: id } : undefined,
});
}
}
export class ConflictError extends HTTPException {
constructor(message: string, details?: Record<string, any>) {
super(409, {
message,
cause: details,
});
}
}
export class RateLimitError extends HTTPException {
constructor(retryAfter: number) {
super(429, {
message: 'Too many requests',
cause: { retryAfter },
});
}
}
```
### Usage
```typescript
// Not found
app.get('/posts/:id', (c) => {
const post = findPost(c.req.param('id'));
if (!post) {
throw new NotFoundError('Post', c.req.param('id'));
}
return c.json({ post });
});
// Unauthorized
app.use('/api/*', (c, next) => {
const token = c.req.header('authorization');
if (!token) {
throw new UnauthorizedError('Missing authorization header');
}
return next();
});
// Forbidden
app.delete('/posts/:id', (c) => {
const user = c.get('user');
const post = findPost(c.req.param('id'));
if (post.authorId !== user.id && user.role !== 'admin') {
throw new ForbiddenError('You can only delete your own posts');
}
deletePost(post.id);
return c.json({ deleted: true });
});
// Conflict
app.post('/users', async (c) => {
const { email } = await c.req.json();
const existing = findUserByEmail(email);
if (existing) {
throw new ConflictError('Email already registered', { email });
}
const user = createUser({ email });
return c.json({ user }, 201);
});
```
## Centralized Error Handler
Use `onError` to handle all errors in one place.
### Basic Handler
```typescript
import { HTTPException } from 'hono/http-exception';
import { ZodError } from 'zod';
app.onError((err, c) => {
console.error('Error:', err);
// HTTPException (includes custom classes)
if (err instanceof HTTPException) {
return c.json({
error: err.message,
...(err.cause && { details: err.cause })
}, err.status);
}
// Zod validation errors
if (err instanceof ZodError) {
return c.json({
error: 'Validation failed',
issues: err.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message,
}))
}, 400);
}
// Generic errors
return c.json({
error: 'Internal server error'
}, 500);
});
```
### Production-Safe Handler
```typescript
app.onError((err, c) => {
const isDev = Bun.env.NODE_ENV !== 'production';
// Log error
console.error('Error:', {
message: err.message,
stack: err.stack,
path: c.req.path,
method: c.req.method,
});
// HTTPException
if (err instanceof HTTPException) {
return c.json({
error: err.message,
...(err.cause && { details: err.cause })
}, err.status);
}
// Zod validation
if (err instanceof ZodError) {
return c.json({
error: 'Validation failed',
issues: err.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message,
}))
}, 400);
}
// Generic errors — sanitize in production
return c.json({
error: isDev ? err.message : 'Internal server error',
...(isDev && { stack: err.stack })
}, 500);
});
```
### Structured Error Logging
```typescript
interface ErrorLog {
timestamp: string;
level: 'error' | 'warn';
message: string;
stack?: string;
context: {
path: string;
method: string;
headers?: Record<string, string>;
user?: string;
};
}
app.onError((err, c) => {
const log: ErrorLog = {
timestamp: new Date().toISOString(),
level: err instanceof HTTPException && err.status < 500 ? 'warn' : 'error',
message: err.message,
stack: err.stack,
context: {
path: c.req.path,
method: c.req.method,
user: c.get('user')?.id,
},
};
// Log to external service (e.g., Sentry, LogRocket)
if (log.level === 'error') {
logToExternalService(log);
} else {
console.warn(JSON.stringify(log));
}
// Return response
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
return c.json({ error: 'Internal server error' }, 500);
});
```
## Validation Errors
Handle Zod validation errors with detailed messages.
### Basic Zod Error Handling
```typescript
import { zValidator } from '@hono/zod-validator';
import { ZodError, z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
content: z.string().min(1, 'Content is required'),
tags: z.array(z.string()).max(5, 'Maximum 5 tags allowed'),
});
app.post('/posts', zValidator('json', CreatePostSchema), (c) => {
const data = c.req.valid('json');
// Data is validated
return c.json({ post: createPost(data) }, 201);
});
// Handle validation errors in onError
app.onError((err, c) => {
if (err instanceof ZodError) {
return c.json({
error: 'Validation failed',
issues: err.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
}))
}, 400);
}
// Other errors...
});
```
### Custom Validation Messages
```typescript
const EmailSchema = z.object({
email: z.string()
.email('Invalid email address')
.refine(
(email) => email.endsWith('@example.com'),
'Email must be from example.com domain'
),
});
app.post('/validate-email', zValidator('json', EmailSchema), (c) => {
const { email } = c.req.valid('json');
return c.json({ valid: true, email });
});
```
### Field-Level Error Formatting
```typescript
app.onError((err, c) => {
if (err instanceof ZodError) {
// Group errors by field
const fieldErrors: Record<string, string[]> = {};
for (const issue of err.issues) {
const field = issue.path.join('.');
if (!fieldErrors[field]) {
fieldErrors[field] = [];
}
fieldErrors[field].push(issue.message);
}
return c.json({
error: 'Validation failed',
fields: fieldErrors,
}, 400);
}
// Other errors...
});
// Example response:
// {
// "error": "Validation failed",
// "fields": {
// "email": ["Invalid email address"],
// "password": ["Password must be at least 8 characters"],
// "tags": ["Maximum 5 tags allowed"]
// }
// }
```
## Not Found Handler
Handle 404 errors for undefined routes.
```typescript
app.notFound((c) => {
return c.json({
error: 'Not found',
path: c.req.path,
}, 404);
});
```
## Error Recovery
### Graceful Degradation
```typescript
app.get('/data', async (c) => {
try {
// Try primary data source
const data = await fetchFromPrimaryAPI();
return c.json({ data, source: 'primary' });
} catch (primaryErr) {
console.warn('Primary API failed, trying backup:', primaryErr);
try {
// Fall back to secondary source
const data = await fetchFromBackupAPI();
return c.json({ data, source: 'backup' });
} catch (backupErr) {
console.error('Both APIs failed:', backupErr);
// Return cached data if available
const cached = getCachedData();
if (cached) {
return c.json({ data: cached, source: 'cache' });
}
throw new HTTPException(503, {
message: 'Service temporarily unavailable',
});
}
}
});
```
### Retry Logic
```typescript
async function retryOperation<T>(
operation: () => Promise<T>,
maxRetries = 3,
delay = 1000
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (err) {
if (attempt === maxRetries) {
throw err;
}
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Retry logic failed');
}
app.get('/external-data', async (c) => {
try {
const data = await retryOperation(() => fetchExternalAPI());
return c.json({ data });
} catch (err) {
throw new HTTPException(503, {
message: 'External service unavailable',
});
}
});
```
### Circuit Breaker
```typescript
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold = 5,
private timeout = 60000 // 1 minute
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > this.timeout) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await operation();
if (this.state === 'half-open') {
this.state = 'closed';
this.failures = 0;
}
return result;
} catch (err) {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
throw err;
}
}
}
const apiCircuitBreaker = new CircuitBreaker();
app.get('/api/data', async (c) => {
try {
const data = await apiCircuitBreaker.execute(() => fetchExternalAPI());
return c.json({ data });
} catch (err) {
if (err.message === 'Circuit breaker is open') {
throw new HTTPException(503, {
message: 'Service temporarily unavailable',
});
}
throw err;
}
});
```
## Database Error Handling
### SQLite Errors
```typescript
app.post('/users', async (c) => {
const { email, name } = await c.req.json();
const db = c.get('db');
try {
const user = db.query(
'INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *'
).get(crypto.randomUUID(), email, name);
return c.json({ user }, 201);
} catch (err: any) {
// SQLite unique constraint violation
if (err.message.includes('UNIQUE constraint failed')) {
throw new ConflictError('Email already registered', { email });
}
// SQLite foreign key constraint
if (err.message.includes('FOREIGN KEY constraint failed')) {
throw new ValidationError('Invalid reference');
}
// Generic database error
console.error('Database error:', err);
throw new HTTPException(500, { message: 'Database error' });
}
});
```
### Transaction Rollback
```typescript
app.post('/transfer', async (c) => {
const { fromId, toId, amount } = await c.req.json();
const db = c.get('db');
try {
db.transaction(() => {
// Deduct from sender
const sender = db.query(
'UPDATE accounts SET balance = balance - ? WHERE id = ? RETURNING balance'
).get(amount, fromId);
if (!sender || sender.balance < 0) {
throw new ValidationError('Insufficient funds');
}
// Add to recipient
db.query(
'UPDATE accounts SET balance = balance + ? WHERE id = ?'
).run(amount, toId);
})();
return c.json({ success: true });
} catch (err) {
if (err instanceof ValidationError) {
throw err;
}
console.error('Transfer failed:', err);
throw new HTTPException(500, { message: 'Transfer failed' });
}
});
```
## Async Error Handling
### Promise Rejection
```typescript
// ❌ Unhandled promise rejection
app.get('/data', (c) => {
fetchData().then(data => {
// This won't work — response already sent
return c.json({ data });
});
return c.json({ loading: true }); // Wrong!
});
// ✅ Await async operations
app.get('/data', async (c) => {
const data = await fetchData();
return c.json({ data });
});
// ✅ Explicit error handling
app.get('/data', async (c) => {
try {
const data = await fetchData();
return c.json({ data });
} catch (err) {
throw new HTTPException(500, { message: 'Failed to fetch data' });
}
});
```
### Parallel Operations
```typescript
app.get('/dashboard', async (c) => {
try {
const [user, posts, stats] = await Promise.all([
fetchUser(c.get('user').id),
fetchUserPosts(c.get('user').id),
fetchUserStats(c.get('user').id),
]);
return c.json({ user, posts, stats });
} catch (err) {
console.error('Dashboard fetch failed:', err);
throw new HTTPException(500, { message: 'Failed to load dashboard' });
}
});
```
### Timeout Handling
```typescript
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
),
]);
}
app.get('/slow-api', async (c) => {
try {
const data = await withTimeout(fetchSlowAPI(), 5000); // 5s timeout
return c.json({ data });
} catch (err) {
if (err.message === 'Operation timed out') {
throw new HTTPException(504, { message: 'Gateway timeout' });
}
throw err;
}
});
```
## Error Response Format
### Consistent Structure
```typescript
interface ErrorResponse {
error: string;
details?: Record<string, any>;
timestamp?: string;
requestId?: string;
}
app.onError((err, c) => {
const response: ErrorResponse = {
error: err.message,
timestamp: new Date().toISOString(),
requestId: c.get('requestId'),
};
if (err instanceof HTTPException && err.cause) {
response.details = err.cause;
}
const status = err instanceof HTTPException ? err.status : 500;
return c.json(response, status);
});
```
### API-Specific Formats
```typescript
// JSON:API format
app.onError((err, c) => {
return c.json({
errors: [{
status: err instanceof HTTPException ? err.status.toString() : '500',
title: err.message,
detail: err instanceof HTTPException ? err.cause : undefined,
}]
}, err instanceof HTTPException ? err.status : 500);
});
// RFC 7807 Problem Details
app.onError((err, c) => {
return c.json({
type: 'about:blank',
title: err.message,
status: err instanceof HTTPException ? err.status : 500,
detail: err instanceof HTTPException ? JSON.stringify(err.cause) : undefined,
instance: c.req.path,
}, err instanceof HTTPException ? err.status : 500);
});
```
@@ -0,0 +1,699 @@
# Factory Pattern — Context Typing
`createFactory<Env>()` provides type-safe context variables across middleware and routes.
## Environment Definition
```typescript
import { createFactory } from 'hono/factory';
import type { Database } from 'bun:sqlite';
type Env = {
Variables: {
user: {
id: string;
email: string;
role: 'admin' | 'user' | 'guest';
};
requestId: string;
db: Database;
session: {
id: string;
expiresAt: Date;
};
};
Bindings: {
// Cloudflare Workers bindings (if deploying to CF)
DB: D1Database;
BUCKET: R2Bucket;
API_KEY: string;
};
};
export const factory = createFactory<Env>();
```
## Typed Middleware
### Basic Middleware
```typescript
// Request ID middleware
export const requestIdMiddleware = factory.createMiddleware(async (c, next) => {
const requestId = c.req.header('x-request-id') || crypto.randomUUID();
c.set('requestId', requestId);
await next();
// Add to response
c.res.headers.set('x-request-id', requestId);
});
// Database middleware
export const dbMiddleware = factory.createMiddleware(async (c, next) => {
const db = new Database('app.db');
c.set('db', db);
try {
await next();
} finally {
db.close(); // Cleanup
}
});
```
### Authentication Middleware
```typescript
import { HTTPException } from 'hono/http-exception';
export const authMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) {
throw new HTTPException(401, { message: 'Missing authorization token' });
}
// Verify token (simplified)
const payload = await verifyJWT(token);
if (!payload) {
throw new HTTPException(401, { message: 'Invalid token' });
}
const db = c.get('db');
const user = db.query('SELECT * FROM users WHERE id = ?').get(payload.userId);
if (!user) {
throw new HTTPException(401, { message: 'User not found' });
}
c.set('user', {
id: user.id,
email: user.email,
role: user.role,
});
await next();
});
// Optional auth — doesn't throw if no token
export const optionalAuthMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (token) {
try {
const payload = await verifyJWT(token);
const db = c.get('db');
const user = db.query('SELECT * FROM users WHERE id = ?').get(payload.userId);
if (user) {
c.set('user', {
id: user.id,
email: user.email,
role: user.role,
});
}
} catch {
// Ignore invalid tokens for optional auth
}
}
await next();
});
```
### Authorization Middleware
```typescript
type Role = 'admin' | 'user' | 'guest';
export const requireRole = (requiredRole: Role) => {
return factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
// Admin has access to everything
if (user.role === 'admin') {
await next();
return;
}
// Check role hierarchy
const roleHierarchy: Record<Role, number> = {
guest: 0,
user: 1,
admin: 2,
};
if (roleHierarchy[user.role] < roleHierarchy[requiredRole]) {
throw new HTTPException(403, {
message: `${requiredRole} access required`,
});
}
await next();
});
};
// Resource ownership check
export const requireOwnership = (resourceKey: 'userId' | 'authorId' = 'userId') => {
return factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
// Admin bypasses ownership check
if (user.role === 'admin') {
await next();
return;
}
// Get resource ID from path params
const resourceUserId = c.req.param(resourceKey);
if (user.id !== resourceUserId) {
throw new HTTPException(403, { message: 'Access denied' });
}
await next();
});
};
```
### Session Middleware
```typescript
export const sessionMiddleware = factory.createMiddleware(async (c, next) => {
const sessionId = c.req.header('x-session-id');
if (!sessionId) {
throw new HTTPException(401, { message: 'Missing session' });
}
const db = c.get('db');
const session = db.query(
'SELECT * FROM sessions WHERE id = ? AND expires_at > CURRENT_TIMESTAMP'
).get(sessionId);
if (!session) {
throw new HTTPException(401, { message: 'Invalid or expired session' });
}
c.set('session', {
id: session.id,
expiresAt: new Date(session.expires_at),
});
// Extend session on activity
db.run(
'UPDATE sessions SET expires_at = datetime(CURRENT_TIMESTAMP, "+1 hour") WHERE id = ?',
[sessionId]
);
await next();
});
```
## Typed Handlers
### Basic Handlers
```typescript
// Single handler
const getProfile = factory.createHandlers((c) => {
const user = c.get('user'); // Fully typed!
const requestId = c.get('requestId');
return c.json({
user,
requestId,
});
});
// Multiple handlers (middleware + handler)
const getUsers = factory.createHandlers(
// Middleware
async (c, next) => {
console.log('Fetching users...');
await next();
},
// Handler
async (c) => {
const db = c.get('db');
const users = db.query('SELECT id, email, role FROM users').all();
return c.json({ users });
}
);
```
### Handlers with Validation
```typescript
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const UpdateProfileSchema = z.object({
name: z.string().min(1).max(100).optional(),
bio: z.string().max(500).optional(),
});
const updateProfile = factory.createHandlers(
zValidator('json', UpdateProfileSchema),
async (c) => {
const user = c.get('user');
const data = c.req.valid('json');
const db = c.get('db');
const updated = db.query(`
UPDATE users
SET name = COALESCE(?, name),
bio = COALESCE(?, bio)
WHERE id = ?
RETURNING *
`).get(data.name || null, data.bio || null, user.id);
return c.json({ user: updated });
}
);
```
## App Assembly
### Simple App
```typescript
const app = factory.createApp()
// Global middleware
.use('*', requestIdMiddleware)
.use('*', dbMiddleware)
// Public routes
.get('/health', (c) => c.json({ status: 'ok' }))
.post('/auth/login', loginHandler)
// Protected routes
.use('/api/*', authMiddleware)
.get('/api/profile', ...getProfile)
.put('/api/profile', ...updateProfile)
// Admin routes
.use('/api/admin/*', requireRole('admin'))
.get('/api/admin/users', ...getUsers);
export type AppType = typeof app;
export default app;
```
### Multi-Module App
```typescript
// routes/users.ts
import { factory } from '../factory';
import { requireRole } from '../middleware/auth';
export const usersRoute = factory.createApp()
.get('/', async (c) => {
const db = c.get('db');
const users = db.query('SELECT id, email, role FROM users').all();
return c.json({ users });
})
.get('/:id', async (c) => {
const db = c.get('db');
const user = db.query('SELECT id, email, role FROM users WHERE id = ?')
.get(c.req.param('id'));
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ user });
})
.use(requireRole('admin')) // Admin-only routes below
.delete('/:id', async (c) => {
const db = c.get('db');
const user = db.query('DELETE FROM users WHERE id = ? RETURNING *')
.get(c.req.param('id'));
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ deleted: true, user });
});
// routes/posts.ts
import { factory } from '../factory';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
});
export const postsRoute = factory.createApp()
.get('/', async (c) => {
const db = c.get('db');
const posts = db.query('SELECT * FROM posts ORDER BY created_at DESC').all();
return c.json({ posts });
})
.post('/', zValidator('json', CreatePostSchema), async (c) => {
const user = c.get('user');
const data = c.req.valid('json');
const db = c.get('db');
const post = db.query(`
INSERT INTO posts (id, user_id, title, content)
VALUES (?, ?, ?, ?)
RETURNING *
`).get(crypto.randomUUID(), user.id, data.title, data.content);
return c.json({ post }, 201);
})
.get('/:id', async (c) => {
const db = c.get('db');
const post = db.query('SELECT * FROM posts WHERE id = ?')
.get(c.req.param('id'));
if (!post) {
throw new HTTPException(404, { message: 'Post not found' });
}
return c.json({ post });
});
// index.ts
import { factory } from './factory';
import { usersRoute } from './routes/users';
import { postsRoute } from './routes/posts';
const app = factory.createApp()
.use('*', requestIdMiddleware)
.use('*', dbMiddleware)
// Mount routes
.route('/users', usersRoute)
.route('/posts', postsRoute);
export type AppType = typeof app;
export default app;
```
## Type Propagation
### Extending Environment
```typescript
// base-env.ts
export type BaseEnv = {
Variables: {
requestId: string;
db: Database;
};
};
// auth-env.ts
import type { BaseEnv } from './base-env';
export type AuthEnv = BaseEnv & {
Variables: BaseEnv['Variables'] & {
user: {
id: string;
role: 'admin' | 'user';
};
};
};
// Usage
const authFactory = createFactory<AuthEnv>();
export const authRoute = authFactory.createApp()
.get('/profile', (c) => {
const user = c.get('user'); // Typed!
const requestId = c.get('requestId'); // Also typed!
const db = c.get('db'); // Also typed!
return c.json({ user, requestId });
});
```
### Merging Environments
```typescript
type Env1 = {
Variables: {
foo: string;
};
};
type Env2 = {
Variables: {
bar: number;
};
};
type MergedEnv = {
Variables: Env1['Variables'] & Env2['Variables'];
};
const factory = createFactory<MergedEnv>();
const app = factory.createApp()
.get('/test', (c) => {
const foo = c.get('foo'); // string
const bar = c.get('bar'); // number
return c.json({ foo, bar });
});
```
## Advanced Patterns
### Conditional Middleware
```typescript
export const conditionalAuth = (condition: (c: Context) => boolean) => {
return factory.createMiddleware(async (c, next) => {
if (condition(c)) {
// Apply auth
await authMiddleware(c, next);
} else {
// Skip auth
await next();
}
});
};
// Usage
const app = factory.createApp()
.use('/api/*', conditionalAuth((c) => {
// Skip auth for health checks
return c.req.path !== '/api/health';
}))
.get('/api/health', (c) => c.json({ status: 'ok' }))
.get('/api/profile', (c) => {
const user = c.get('user'); // May be undefined
return c.json({ user });
});
```
### Middleware Composition
```typescript
const composeMiddleware = (...middlewares: MiddlewareHandler[]) => {
return factory.createMiddleware(async (c, next) => {
const execute = async (index: number) => {
if (index >= middlewares.length) {
await next();
return;
}
await middlewares[index](c, async () => {
await execute(index + 1);
});
};
await execute(0);
});
};
// Usage
const app = factory.createApp()
.use('/api/*', composeMiddleware(
requestIdMiddleware,
dbMiddleware,
authMiddleware
))
.get('/api/profile', (c) => {
// All middleware ran
const requestId = c.get('requestId');
const db = c.get('db');
const user = c.get('user');
return c.json({ user, requestId });
});
```
### Scoped Factories
```typescript
// Public routes — no auth
const publicFactory = createFactory<{
Variables: {
requestId: string;
db: Database;
};
}>();
export const publicRoute = publicFactory.createApp()
.get('/status', (c) => {
// No user available here
return c.json({ status: 'ok' });
});
// Protected routes — auth required
const protectedFactory = createFactory<{
Variables: {
requestId: string;
db: Database;
user: { id: string; role: string };
};
}>();
export const protectedRoute = protectedFactory.createApp()
.get('/profile', (c) => {
const user = c.get('user'); // Always available!
return c.json({ user });
});
// Combine
const app = factory.createApp()
.use('*', requestIdMiddleware)
.use('*', dbMiddleware)
.route('/public', publicRoute)
.use('/protected/*', authMiddleware)
.route('/protected', protectedRoute);
```
### Dependency Injection
```typescript
interface IDatabase {
query(sql: string): any;
}
interface ICache {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
}
type Env = {
Variables: {
db: IDatabase;
cache: ICache;
user: { id: string };
};
};
const factory = createFactory<Env>();
// Inject dependencies
const createApp = (db: IDatabase, cache: ICache) => {
return factory.createApp()
.use('*', async (c, next) => {
c.set('db', db);
c.set('cache', cache);
await next();
})
.get('/users/:id', async (c) => {
const cache = c.get('cache');
const db = c.get('db');
const id = c.req.param('id');
// Try cache first
const cached = await cache.get(`user:${id}`);
if (cached) {
return c.json(JSON.parse(cached));
}
// Fetch from DB
const user = db.query('SELECT * FROM users WHERE id = ?').get(id);
// Cache result
await cache.set(`user:${id}`, JSON.stringify(user));
return c.json({ user });
});
};
// Usage
const db = new Database('app.db');
const cache = new RedisClient();
const app = createApp(db, cache);
```
## Common Pitfalls
```typescript
// ❌ Wrong: Variables set but not in type
type Env = {
Variables: {
user: { id: string };
};
};
const factory = createFactory<Env>();
const app = factory.createApp()
.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID()); // Type error!
await next();
});
// ✅ Correct: Include all variables in type
type Env = {
Variables: {
user: { id: string };
requestId: string; // Added!
};
};
// ❌ Wrong: Using base Hono with factory
import { Hono } from 'hono';
const app = new Hono() // Lost types!
.use(authMiddleware) // Middleware expects typed context
.get('/profile', (c) => {
const user = c.get('user'); // Type error!
});
// ✅ Correct: Use factory.createApp()
const app = factory.createApp()
.use(authMiddleware)
.get('/profile', (c) => {
const user = c.get('user'); // Fully typed!
});
// ❌ Wrong: Middleware doesn't use factory
const authMiddleware = async (c: Context, next: Next) => {
c.set('user', { id: '123' }); // Lost types!
await next();
};
// ✅ Correct: Use factory.createMiddleware
const authMiddleware = factory.createMiddleware(async (c, next) => {
c.set('user', { id: '123' }); // Typed!
await next();
});
```
@@ -0,0 +1,457 @@
# Middleware Patterns
Common middleware patterns for Hono APIs.
## Built-in Middleware
### Logger
```typescript
import { logger } from 'hono/logger';
app.use('*', logger());
// Custom log function
app.use('*', logger((message) => {
console.log(`[${new Date().toISOString()}] ${message}`);
}));
```
### CORS
```typescript
import { cors } from 'hono/cors';
// Basic CORS
app.use('/api/*', cors());
// Configured CORS
app.use('/api/*', cors({
origin: ['http://localhost:3000', 'https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400, // 24 hours
}));
// Dynamic origin
app.use('/api/*', cors({
origin: (origin) => {
if (origin.endsWith('.example.com')) {
return origin;
}
return null;
},
}));
```
### Compress
```typescript
import { compress } from 'hono/compress';
app.use('*', compress());
```
### Secure Headers
```typescript
import { secureHeaders } from 'hono/secure-headers';
app.use('*', secureHeaders());
```
### Bearer Auth
```typescript
import { bearerAuth } from 'hono/bearer-auth';
app.use('/api/*', bearerAuth({
token: Bun.env.API_TOKEN!,
}));
// Multiple tokens
app.use('/api/*', bearerAuth({
token: [Bun.env.API_TOKEN!, Bun.env.ADMIN_TOKEN!],
}));
// Custom verification
app.use('/api/*', bearerAuth({
verifyToken: async (token, c) => {
const user = await verifyJWT(token);
if (user) {
c.set('user', user);
return true;
}
return false;
},
}));
```
### Basic Auth
```typescript
import { basicAuth } from 'hono/basic-auth';
app.use('/admin/*', basicAuth({
username: 'admin',
password: Bun.env.ADMIN_PASSWORD!,
}));
```
## Custom Middleware with Factory
### Authentication
```typescript
import { createFactory } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
type Env = {
Variables: {
user: { id: string; email: string; role: 'admin' | 'user' };
};
};
const factory = createFactory<Env>();
export const authMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) {
throw new HTTPException(401, { message: 'Missing authorization token' });
}
const payload = await verifyJWT(token);
if (!payload) {
throw new HTTPException(401, { message: 'Invalid token' });
}
c.set('user', {
id: payload.sub,
email: payload.email,
role: payload.role,
});
await next();
});
```
### Optional Authentication
```typescript
export const optionalAuth = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (token) {
try {
const payload = await verifyJWT(token);
if (payload) {
c.set('user', {
id: payload.sub,
email: payload.email,
role: payload.role,
});
}
} catch {
// Invalid token, continue without user
}
}
await next();
});
```
### Role-Based Access Control
```typescript
export const requireRole = (requiredRole: 'admin' | 'user') => {
return factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
// Admin has access to everything
if (user.role === 'admin') {
await next();
return;
}
if (user.role !== requiredRole) {
throw new HTTPException(403, { message: `${requiredRole} access required` });
}
await next();
});
};
// Usage
app.use('/api/admin/*', requireRole('admin'));
```
### Resource Ownership
```typescript
export const requireOwnership = (paramName = 'userId') => {
return factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
// Admin bypasses ownership check
if (user.role === 'admin') {
await next();
return;
}
const resourceUserId = c.req.param(paramName);
if (user.id !== resourceUserId) {
throw new HTTPException(403, { message: 'Access denied' });
}
await next();
});
};
// Usage
app.delete('/users/:userId', requireOwnership('userId'), deleteUser);
```
### Request ID
```typescript
export const requestIdMiddleware = factory.createMiddleware(async (c, next) => {
const requestId = c.req.header('x-request-id') || crypto.randomUUID();
c.set('requestId', requestId);
await next();
c.res.headers.set('x-request-id', requestId);
});
```
### Request Timing
```typescript
export const timingMiddleware = factory.createMiddleware(async (c, next) => {
const start = Bun.nanoseconds();
await next();
const duration = (Bun.nanoseconds() - start) / 1_000_000;
c.res.headers.set('x-response-time', `${duration.toFixed(2)}ms`);
console.log(`${c.req.method} ${c.req.path} - ${duration.toFixed(2)}ms`);
});
```
### Rate Limiting
```typescript
const rateLimits = new Map<string, { count: number; resetAt: number }>();
export const rateLimiter = (limit: number, windowMs: number) => {
return factory.createMiddleware(async (c, next) => {
const ip = c.req.header('x-forwarded-for') || 'unknown';
const now = Date.now();
const entry = rateLimits.get(ip);
if (!entry || now > entry.resetAt) {
rateLimits.set(ip, { count: 1, resetAt: now + windowMs });
} else {
entry.count++;
if (entry.count > limit) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
throw new HTTPException(429, {
message: 'Rate limit exceeded',
cause: { retryAfter },
});
}
}
await next();
});
};
// Usage: 100 requests per minute
app.use('/api/*', rateLimiter(100, 60 * 1000));
```
### Database Connection
```typescript
import { Database } from 'bun:sqlite';
export const dbMiddleware = factory.createMiddleware(async (c, next) => {
const db = new Database('app.db');
c.set('db', db);
try {
await next();
} finally {
db.close();
}
});
// With connection pooling
class DatabasePool {
private pool: Database[] = [];
get(): Database {
return this.pool.pop() || new Database('app.db');
}
release(db: Database) {
this.pool.push(db);
}
}
const pool = new DatabasePool();
export const pooledDbMiddleware = factory.createMiddleware(async (c, next) => {
const db = pool.get();
c.set('db', db);
try {
await next();
} finally {
pool.release(db);
}
});
```
### Caching
```typescript
const cache = new Map<string, { data: any; expiresAt: number }>();
export const cacheMiddleware = (ttlMs: number) => {
return factory.createMiddleware(async (c, next) => {
if (c.req.method !== 'GET') {
await next();
return;
}
const key = c.req.url;
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
return c.json(cached.data);
}
await next();
// Cache response after handler
const response = c.res.clone();
const data = await response.json();
cache.set(key, {
data,
expiresAt: Date.now() + ttlMs,
});
});
};
// Usage: 5 minute cache
app.get('/api/public-data', cacheMiddleware(5 * 60 * 1000), handler);
```
### Request Validation
```typescript
import { z } from 'zod';
export const validateRequest = <T extends z.ZodType>(schema: T) => {
return factory.createMiddleware(async (c, next) => {
try {
const body = await c.req.json();
schema.parse(body);
} catch (err) {
if (err instanceof z.ZodError) {
throw new HTTPException(400, {
message: 'Validation failed',
cause: err.issues,
});
}
throw err;
}
await next();
});
};
```
## Middleware Composition
```typescript
// Compose multiple middleware
const apiMiddleware = factory.createMiddleware(async (c, next) => {
// Request ID
c.set('requestId', crypto.randomUUID());
// Timing start
const start = Bun.nanoseconds();
await next();
// Timing end
const duration = (Bun.nanoseconds() - start) / 1_000_000;
c.res.headers.set('x-request-id', c.get('requestId'));
c.res.headers.set('x-response-time', `${duration.toFixed(2)}ms`);
});
// Apply composed middleware
app.use('/api/*', apiMiddleware);
```
## Conditional Middleware
```typescript
export const conditionalAuth = (condition: (c: Context) => boolean) => {
return factory.createMiddleware(async (c, next) => {
if (condition(c)) {
await authMiddleware(c, next);
} else {
await next();
}
});
};
// Skip auth for health checks
app.use('/api/*', conditionalAuth((c) => c.req.path !== '/api/health'));
```
## Middleware Order
```typescript
const app = factory.createApp()
// Global middleware (runs for all routes)
.use('*', logger())
.use('*', requestIdMiddleware)
.use('*', timingMiddleware)
// API middleware
.use('/api/*', cors())
.use('/api/*', dbMiddleware)
// Public routes (before auth middleware)
.get('/api/health', (c) => c.json({ status: 'ok' }))
.post('/api/auth/login', loginHandler)
// Protected routes
.use('/api/*', authMiddleware)
.get('/api/profile', profileHandler)
.get('/api/users', usersHandler)
// Admin routes
.use('/api/admin/*', requireRole('admin'))
.get('/api/admin/stats', statsHandler);
```
@@ -0,0 +1,839 @@
# Zod OpenAPI Integration
Schema-first API development with automatic OpenAPI specification generation.
## Installation
```bash
bun add @hono/zod-openapi
bun add @hono/swagger-ui
```
## Basic Setup
```typescript
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
import { swaggerUI } from '@hono/swagger-ui';
const app = new OpenAPIHono();
// Define routes (see below)
// Generate OpenAPI spec
app.doc('/openapi.json', {
openapi: '3.1.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation',
},
servers: [
{ url: 'http://localhost:3000', description: 'Development' },
{ url: 'https://api.example.com', description: 'Production' },
],
});
// Swagger UI
app.get('/docs', swaggerUI({ url: '/openapi.json' }));
export default app;
```
## Schema Definition
### Basic Schemas
```typescript
// Register schemas for reuse
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
}).openapi('User'); // Register with name
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
}).openapi('CreateUser');
const UpdateUserSchema = CreateUserSchema.partial().openapi('UpdateUser');
const ErrorSchema = z.object({
error: z.string(),
details: z.record(z.any()).optional(),
}).openapi('Error');
```
### Schema with Examples
```typescript
const ProductSchema = z.object({
id: z.string().uuid().openapi({
example: '123e4567-e89b-12d3-a456-426614174000',
}),
name: z.string().min(1).max(200).openapi({
example: 'Laptop',
}),
price: z.number().positive().openapi({
example: 999.99,
}),
category: z.enum(['electronics', 'clothing', 'books']).openapi({
example: 'electronics',
}),
tags: z.array(z.string()).optional().openapi({
example: ['gaming', 'portable'],
}),
}).openapi('Product');
```
### Schema with Descriptions
```typescript
const PostSchema = z.object({
id: z.string().uuid().describe('Unique post identifier'),
title: z.string().min(1).max(200).describe('Post title'),
content: z.string().min(1).describe('Post content (markdown supported)'),
published: z.boolean().default(false).describe('Publication status'),
author: UserSchema.describe('Post author'),
tags: z.array(z.string()).optional().describe('Post tags for categorization'),
createdAt: z.string().datetime().describe('Creation timestamp'),
updatedAt: z.string().datetime().describe('Last update timestamp'),
}).openapi('Post');
```
## Route Definition
### GET Route
```typescript
const getUserRoute = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: z.object({
id: z.string().uuid().openapi({
param: { name: 'id', in: 'path' },
example: '123e4567-e89b-12d3-a456-426614174000',
}),
}),
},
responses: {
200: {
content: {
'application/json': { schema: UserSchema },
},
description: 'User found',
},
404: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'User not found',
},
},
tags: ['Users'],
summary: 'Get user by ID',
description: 'Retrieves a single user by their UUID',
});
app.openapi(getUserRoute, (c) => {
const { id } = c.req.valid('param'); // Typed!
const user = db.query('SELECT * FROM users WHERE id = ?').get(id);
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(user, 200);
});
```
### POST Route
```typescript
const createUserRoute = createRoute({
method: 'post',
path: '/users',
request: {
body: {
content: {
'application/json': { schema: CreateUserSchema },
},
description: 'User data',
required: true,
},
},
responses: {
201: {
content: {
'application/json': { schema: UserSchema },
},
description: 'User created',
},
400: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'Validation error',
},
},
tags: ['Users'],
summary: 'Create new user',
});
app.openapi(createUserRoute, async (c) => {
const data = c.req.valid('json'); // Typed as CreateUserSchema!
const hashedPassword = await Bun.password.hash(data.password);
const user = db.query(`
INSERT INTO users (id, email, name, password)
VALUES (?, ?, ?, ?)
RETURNING id, email, name, role, created_at as createdAt
`).get(crypto.randomUUID(), data.email, data.name, hashedPassword);
return c.json(user, 201);
});
```
### PUT/PATCH Routes
```typescript
const updateUserRoute = createRoute({
method: 'put',
path: '/users/{id}',
request: {
params: z.object({
id: z.string().uuid(),
}),
body: {
content: {
'application/json': { schema: UpdateUserSchema },
},
},
},
responses: {
200: {
content: {
'application/json': { schema: UserSchema },
},
description: 'User updated',
},
404: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'User not found',
},
},
tags: ['Users'],
});
app.openapi(updateUserRoute, async (c) => {
const { id } = c.req.valid('param');
const data = c.req.valid('json');
const user = db.query(`
UPDATE users
SET email = COALESCE(?, email),
name = COALESCE(?, name)
WHERE id = ?
RETURNING id, email, name, role, created_at as createdAt
`).get(data.email || null, data.name || null, id);
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(user, 200);
});
```
### DELETE Route
```typescript
const deleteUserRoute = createRoute({
method: 'delete',
path: '/users/{id}',
request: {
params: z.object({
id: z.string().uuid(),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
deleted: z.boolean(),
user: UserSchema,
}),
},
},
description: 'User deleted',
},
404: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'User not found',
},
},
tags: ['Users'],
});
app.openapi(deleteUserRoute, (c) => {
const { id } = c.req.valid('param');
const user = db.query('DELETE FROM users WHERE id = ? RETURNING *').get(id);
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
return c.json({ deleted: true, user }, 200);
});
```
## Query Parameters
```typescript
const PaginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
}).openapi('Pagination');
const listUsersRoute = createRoute({
method: 'get',
path: '/users',
request: {
query: PaginationSchema,
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
users: z.array(UserSchema),
total: z.number(),
page: z.number(),
limit: z.number(),
totalPages: z.number(),
}),
},
},
description: 'Users list',
},
},
tags: ['Users'],
});
app.openapi(listUsersRoute, (c) => {
const { page, limit } = c.req.valid('query'); // Typed with defaults!
const offset = (page - 1) * limit;
const users = db.query(
'SELECT * FROM users LIMIT ? OFFSET ?'
).all(limit, offset);
const total = db.query('SELECT COUNT(*) as count FROM users')
.get() as { count: number };
return c.json({
users,
total: total.count,
page,
limit,
totalPages: Math.ceil(total.count / limit),
});
});
```
## Headers
```typescript
const protectedRoute = createRoute({
method: 'get',
path: '/protected',
request: {
headers: z.object({
authorization: z.string().openapi({
example: 'Bearer token123',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({ protected: z.boolean() }),
},
},
description: 'Success',
},
401: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'Unauthorized',
},
},
tags: ['Auth'],
security: [{ bearerAuth: [] }],
});
app.openapi(protectedRoute, (c) => {
const { authorization } = c.req.valid('header');
// Verify token...
return c.json({ protected: true });
});
```
## Security Schemes
```typescript
app.doc('/openapi.json', {
openapi: '3.1.0',
info: {
title: 'My API',
version: '1.0.0',
},
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
apiKey: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key',
},
oauth2: {
type: 'oauth2',
flows: {
authorizationCode: {
authorizationUrl: 'https://example.com/oauth/authorize',
tokenUrl: 'https://example.com/oauth/token',
scopes: {
'read:users': 'Read user data',
'write:users': 'Create and update users',
},
},
},
},
},
},
});
// Use in routes
const secureRoute = createRoute({
method: 'get',
path: '/secure',
security: [
{ bearerAuth: [] },
{ apiKey: [] },
],
// ...
});
```
## Response Types
### Multiple Content Types
```typescript
const getFileRoute = createRoute({
method: 'get',
path: '/files/{id}',
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
id: z.string(),
name: z.string(),
url: z.string(),
}),
},
'application/octet-stream': {
schema: z.instanceof(Blob),
},
},
description: 'File metadata or content',
},
},
});
app.openapi(getFileRoute, (c) => {
const { id } = c.req.valid('param');
const accept = c.req.header('accept');
const file = findFile(id);
if (accept?.includes('application/octet-stream')) {
return c.body(file.stream());
}
return c.json({ id: file.id, name: file.name, url: file.url });
});
```
### Status Code Unions
```typescript
const route = createRoute({
method: 'post',
path: '/action',
responses: {
200: {
content: {
'application/json': {
schema: z.object({ success: z.literal(true) }),
},
},
description: 'Success',
},
202: {
content: {
'application/json': {
schema: z.object({ accepted: z.literal(true) }),
},
},
description: 'Accepted for processing',
},
400: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'Bad request',
},
},
});
app.openapi(route, async (c) => {
const result = await processAction();
if (result.immediate) {
return c.json({ success: true }, 200);
}
return c.json({ accepted: true }, 202);
});
```
## Nested Resources
```typescript
const getPostCommentsRoute = createRoute({
method: 'get',
path: '/posts/{postId}/comments',
request: {
params: z.object({
postId: z.string().uuid(),
}),
query: PaginationSchema,
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
comments: z.array(CommentSchema),
total: z.number(),
}),
},
},
description: 'Comments list',
},
404: {
content: {
'application/json': { schema: ErrorSchema },
},
description: 'Post not found',
},
},
tags: ['Comments'],
});
const createCommentRoute = createRoute({
method: 'post',
path: '/posts/{postId}/comments',
request: {
params: z.object({
postId: z.string().uuid(),
}),
body: {
content: {
'application/json': {
schema: z.object({
content: z.string().min(1),
}),
},
},
},
},
responses: {
201: {
content: {
'application/json': { schema: CommentSchema },
},
description: 'Comment created',
},
},
tags: ['Comments'],
});
```
## Grouping Routes
```typescript
// Create separate apps for different resources
const usersApp = new OpenAPIHono();
usersApp.openapi(getUserRoute, getUserHandler);
usersApp.openapi(createUserRoute, createUserHandler);
usersApp.openapi(updateUserRoute, updateUserHandler);
usersApp.openapi(deleteUserRoute, deleteUserHandler);
const postsApp = new OpenAPIHono();
postsApp.openapi(getPostRoute, getPostHandler);
postsApp.openapi(createPostRoute, createPostHandler);
// Combine
const app = new OpenAPIHono()
.route('/users', usersApp)
.route('/posts', postsApp);
// Generate combined OpenAPI spec
app.doc('/openapi.json', {
openapi: '3.1.0',
info: {
title: 'Combined API',
version: '1.0.0',
},
});
```
## Tags and Organization
```typescript
app.doc('/openapi.json', {
openapi: '3.1.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API with organized endpoints',
},
tags: [
{
name: 'Users',
description: 'User management endpoints',
},
{
name: 'Posts',
description: 'Blog post endpoints',
},
{
name: 'Comments',
description: 'Comment management',
},
{
name: 'Admin',
description: 'Administrative endpoints',
externalDocs: {
description: 'Admin guide',
url: 'https://docs.example.com/admin',
},
},
],
});
```
## Custom Validation
```typescript
const EmailSchema = z.string().email().refine(
(email) => email.endsWith('@example.com'),
{ message: 'Email must be from example.com domain' }
).openapi('CompanyEmail');
const PasswordSchema = z.string().min(8).refine(
(password) => {
// Complex password requirements
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*]/.test(password);
return hasUpper && hasLower && hasNumber && hasSpecial;
},
{ message: 'Password must contain uppercase, lowercase, number, and special character' }
).openapi('StrongPassword');
```
## With Factory Pattern
```typescript
import { createFactory } from 'hono/factory';
import { OpenAPIHono } from '@hono/zod-openapi';
type Env = {
Variables: {
user: { id: string; role: string };
db: Database;
};
};
// Use OpenAPIHono directly (doesn't support factory.createApp)
const app = new OpenAPIHono<Env>();
// Create middleware with factory
const factory = createFactory<Env>();
const authMiddleware = factory.createMiddleware(async (c, next) => {
// Auth logic...
c.set('user', { id: '123', role: 'admin' });
await next();
});
// Apply middleware
app.use('*', authMiddleware);
// Define routes
app.openapi(getUserRoute, (c) => {
const user = c.get('user'); // Typed from Env!
const db = c.get('db'); // Typed from Env!
// ...
});
```
## Type Extraction
```typescript
import type { z } from 'zod';
// Extract inferred type from schema
type User = z.infer<typeof UserSchema>;
type CreateUserInput = z.infer<typeof CreateUserSchema>;
type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
// Use in application code
function saveUser(user: User) {
// ...
}
function validateUser(input: CreateUserInput): User {
// ...
}
```
## Testing OpenAPI Routes
```typescript
import { testClient } from 'hono/testing';
describe('OpenAPI Routes', () => {
const client = testClient(app);
test('POST /users validates schema', async () => {
const res = await client.users.$post({
json: {
email: 'invalid-email', // Invalid!
name: 'John',
password: 'pass',
}
});
expect(res.status).toBe(400);
const error = await res.json();
expect(error.error).toBeTruthy();
});
test('GET /openapi.json returns valid spec', async () => {
const res = await client['openapi.json'].$get();
expect(res.status).toBe(200);
const spec = await res.json();
expect(spec.openapi).toBe('3.1.0');
expect(spec.info).toBeTruthy();
expect(spec.paths).toBeTruthy();
});
});
```
## Common Patterns
### Reusable Error Responses
```typescript
const errorResponses = {
400: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'Bad request',
},
401: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'Unauthorized',
},
403: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'Forbidden',
},
404: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'Not found',
},
500: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'Internal server error',
},
};
// Use in routes
const route = createRoute({
method: 'get',
path: '/resource',
responses: {
200: {
content: { 'application/json': { schema: ResourceSchema } },
description: 'Success',
},
...errorResponses, // Spread common errors
},
});
```
### Reusable Request Schemas
```typescript
const authHeaders = z.object({
authorization: z.string(),
});
const route = createRoute({
method: 'get',
path: '/protected',
request: {
headers: authHeaders, // Reuse
},
// ...
});
```