📦 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,738 @@
# Testing Patterns
Type-safe testing with `testClient` — no HTTP server required.
## Basic Setup
```typescript
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import { testClient } from 'hono/testing';
import { Hono } from 'hono';
import { Database } from 'bun:sqlite';
// Simple app for testing
const createApp = () => {
return new Hono()
.get('/health', (c) => c.json({ status: 'ok' }))
.get('/error', () => {
throw new Error('Test error');
});
};
describe('Basic Tests', () => {
test('GET /health returns 200', async () => {
const app = createApp();
const client = testClient(app);
const res = await client.health.$get();
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toEqual({ status: 'ok' });
});
test('GET /error returns 500', async () => {
const app = createApp();
const client = testClient(app);
const res = await client.error.$get();
expect(res.status).toBe(500);
});
});
```
## Testing CRUD Operations
```typescript
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
});
describe('Posts API', () => {
let db: Database;
let app: Hono;
let client: ReturnType<typeof testClient>;
beforeEach(() => {
// In-memory database for each test
db = new Database(':memory:');
db.run(`
CREATE TABLE posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Create app with database
app = new Hono()
.get('/posts', (c) => {
const posts = db.query('SELECT * FROM posts').all();
return c.json({ posts });
})
.get('/posts/:id', (c) => {
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 });
})
.post('/posts', zValidator('json', CreatePostSchema), (c) => {
const data = c.req.valid('json');
const post = db.query(
'INSERT INTO posts (id, title, content) VALUES (?, ?, ?) RETURNING *'
).get(crypto.randomUUID(), data.title, data.content);
return c.json({ post }, 201);
})
.delete('/posts/:id', (c) => {
const post = db.query('DELETE FROM posts WHERE id = ? RETURNING *').get(c.req.param('id'));
if (!post) {
throw new HTTPException(404, { message: 'Post not found' });
}
return c.json({ deleted: true, post });
});
client = testClient(app);
});
afterEach(() => {
db.close();
});
test('GET /posts returns empty array initially', async () => {
const res = await client.posts.$get();
expect(res.status).toBe(200);
const data = await res.json();
expect(data.posts).toEqual([]);
});
test('POST /posts creates post', async () => {
const res = await client.posts.$post({
json: {
title: 'Test Post',
content: 'This is a test post',
}
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.post).toMatchObject({
title: 'Test Post',
content: 'This is a test post',
});
expect(data.post.id).toBeTruthy();
expect(data.post.created_at).toBeTruthy();
});
test('POST /posts validates input', async () => {
const res = await client.posts.$post({
json: { title: '' } as any // Invalid input
});
expect(res.status).toBe(400);
const error = await res.json();
expect(error).toHaveProperty('error');
});
test('GET /posts/:id returns post', async () => {
// Create post first
const createRes = await client.posts.$post({
json: { title: 'Test', content: 'Content' }
});
const { post } = await createRes.json();
// Get post
const res = await client.posts[':id'].$get({
param: { id: post.id }
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.post).toEqual(post);
});
test('GET /posts/:id returns 404 for non-existent post', async () => {
const res = await client.posts[':id'].$get({
param: { id: 'non-existent' }
});
expect(res.status).toBe(404);
const error = await res.json();
expect(error.error).toBe('Post not found');
});
test('DELETE /posts/:id deletes post', async () => {
// Create post
const createRes = await client.posts.$post({
json: { title: 'Test', content: 'Content' }
});
const { post } = await createRes.json();
// Delete post
const deleteRes = await client.posts[':id'].$delete({
param: { id: post.id }
});
expect(deleteRes.status).toBe(200);
const deleteData = await deleteRes.json();
expect(deleteData.deleted).toBe(true);
// Verify deletion
const getRes = await client.posts[':id'].$get({
param: { id: post.id }
});
expect(getRes.status).toBe(404);
});
test('GET /posts returns created posts', async () => {
// Create multiple posts
await client.posts.$post({ json: { title: 'Post 1', content: 'Content 1' } });
await client.posts.$post({ json: { title: 'Post 2', content: 'Content 2' } });
await client.posts.$post({ json: { title: 'Post 3', content: 'Content 3' } });
const res = await client.posts.$get();
const data = await res.json();
expect(data.posts).toHaveLength(3);
expect(data.posts.map((p: any) => p.title)).toEqual(['Post 1', 'Post 2', 'Post 3']);
});
});
```
## Testing Authentication
```typescript
import { createFactory } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
type Env = {
Variables: {
user: { id: string; role: 'admin' | 'user' };
};
};
const factory = createFactory<Env>();
// Mock token verification
const mockUsers = new Map([
['valid-token', { id: 'user-123', role: 'user' as const }],
['admin-token', { id: 'admin-456', role: 'admin' as const }],
]);
const authMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) {
throw new HTTPException(401, { message: 'Missing token' });
}
const user = mockUsers.get(token);
if (!user) {
throw new HTTPException(401, { message: 'Invalid token' });
}
c.set('user', user);
await next();
});
const requireAdmin = factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (user.role !== 'admin') {
throw new HTTPException(403, { message: 'Admin access required' });
}
await next();
});
describe('Authentication', () => {
const app = factory.createApp()
.get('/public', (c) => c.json({ public: true }))
.use('/protected/*', authMiddleware)
.get('/protected/profile', (c) => {
const user = c.get('user');
return c.json({ user });
})
.use('/protected/admin/*', requireAdmin)
.get('/protected/admin/dashboard', (c) => {
return c.json({ admin: true });
});
const client = testClient(app);
test('Public route accessible without auth', async () => {
const res = await client.public.$get();
expect(res.status).toBe(200);
const data = await res.json();
expect(data.public).toBe(true);
});
test('Protected route requires auth', async () => {
const res = await client.protected.profile.$get();
expect(res.status).toBe(401);
const error = await res.json();
expect(error.error).toBe('Missing token');
});
test('Protected route accepts valid token', async () => {
const res = await client.protected.profile.$get({}, {
headers: { Authorization: 'Bearer valid-token' }
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.user).toEqual({ id: 'user-123', role: 'user' });
});
test('Protected route rejects invalid token', async () => {
const res = await client.protected.profile.$get({}, {
headers: { Authorization: 'Bearer invalid-token' }
});
expect(res.status).toBe(401);
const error = await res.json();
expect(error.error).toBe('Invalid token');
});
test('Admin route requires admin role', async () => {
const res = await client.protected.admin.dashboard.$get({}, {
headers: { Authorization: 'Bearer valid-token' }
});
expect(res.status).toBe(403);
const error = await res.json();
expect(error.error).toBe('Admin access required');
});
test('Admin route accepts admin token', async () => {
const res = await client.protected.admin.dashboard.$get({}, {
headers: { Authorization: 'Bearer admin-token' }
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.admin).toBe(true);
});
});
```
## Testing Middleware
```typescript
import { logger } from 'hono/logger';
describe('Middleware', () => {
test('Logger middleware logs requests', async () => {
const logs: string[] = [];
// Custom logger that captures logs
const customLogger = (message: string) => {
logs.push(message);
};
const app = new Hono()
.use('*', logger(customLogger))
.get('/test', (c) => c.json({ ok: true }));
const client = testClient(app);
await client.test.$get();
expect(logs.length).toBeGreaterThan(0);
expect(logs.some(log => log.includes('GET'))).toBe(true);
expect(logs.some(log => log.includes('/test'))).toBe(true);
});
test('Custom middleware runs before handler', async () => {
const executionOrder: string[] = [];
const app = new Hono()
.use('*', async (c, next) => {
executionOrder.push('middleware-before');
await next();
executionOrder.push('middleware-after');
})
.get('/test', (c) => {
executionOrder.push('handler');
return c.json({ ok: true });
});
const client = testClient(app);
await client.test.$get();
expect(executionOrder).toEqual([
'middleware-before',
'handler',
'middleware-after',
]);
});
test('Middleware can modify context', async () => {
type Env = {
Variables: {
requestId: string;
timestamp: number;
};
};
const factory = createFactory<Env>();
const contextMiddleware = factory.createMiddleware(async (c, next) => {
c.set('requestId', crypto.randomUUID());
c.set('timestamp', Date.now());
await next();
});
const app = factory.createApp()
.use('*', contextMiddleware)
.get('/test', (c) => {
return c.json({
requestId: c.get('requestId'),
timestamp: c.get('timestamp'),
});
});
const client = testClient(app);
const res = await client.test.$get();
const data = await res.json();
expect(data.requestId).toBeTruthy();
expect(typeof data.requestId).toBe('string');
expect(data.timestamp).toBeTruthy();
expect(typeof data.timestamp).toBe('number');
});
});
```
## Testing Error Handling
```typescript
import { HTTPException } from 'hono/http-exception';
import { ZodError } from 'zod';
describe('Error Handling', () => {
test('HTTPException returns correct status and message', async () => {
const app = new Hono()
.get('/error', () => {
throw new HTTPException(418, { message: "I'm a teapot" });
})
.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
return c.json({ error: 'Internal error' }, 500);
});
const client = testClient(app);
const res = await client.error.$get();
expect(res.status).toBe(418);
const error = await res.json();
expect(error.error).toBe("I'm a teapot");
});
test('Validation errors handled correctly', async () => {
const Schema = z.object({
email: z.string().email(),
age: z.number().int().positive(),
});
const app = new Hono()
.post('/validate', zValidator('json', Schema), (c) => {
const data = c.req.valid('json');
return c.json({ data });
})
.onError((err, c) => {
if (err instanceof ZodError) {
return c.json({
error: 'Validation failed',
issues: err.issues,
}, 400);
}
return c.json({ error: 'Internal error' }, 500);
});
const client = testClient(app);
const res = await client.validate.$post({
json: { email: 'invalid', age: -1 } as any
});
expect(res.status).toBe(400);
const error = await res.json();
expect(error.error).toBe('Validation failed');
expect(error.issues).toBeTruthy();
expect(Array.isArray(error.issues)).toBe(true);
});
test('Generic errors sanitized in production', async () => {
const originalEnv = process.env.NODE_ENV;
const app = new Hono()
.get('/error', () => {
throw new Error('Sensitive internal error');
})
.onError((err, c) => {
const isDev = process.env.NODE_ENV !== 'production';
return c.json({
error: isDev ? err.message : 'Internal server error'
}, 500);
});
const client = testClient(app);
// Development mode
process.env.NODE_ENV = 'development';
let res = await client.error.$get();
let error = await res.json();
expect(error.error).toBe('Sensitive internal error');
// Production mode
process.env.NODE_ENV = 'production';
res = await client.error.$get();
error = await res.json();
expect(error.error).toBe('Internal server error');
// Restore
process.env.NODE_ENV = originalEnv;
});
});
```
## Integration Testing with Database
```typescript
describe('Integration Tests', () => {
let db: Database;
beforeEach(() => {
db = new Database(':memory:');
// Create schema
db.run(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
db.run(`
CREATE TABLE posts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
});
afterEach(() => {
db.close();
});
test('Full user and post workflow', async () => {
const app = new Hono()
.post('/users', zValidator('json', z.object({
email: z.string().email(),
name: z.string(),
})), (c) => {
const data = c.req.valid('json');
const user = db.query(
'INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *'
).get(crypto.randomUUID(), data.email, data.name);
return c.json({ user }, 201);
})
.get('/users/:id/posts', (c) => {
const posts = db.query(
'SELECT * FROM posts WHERE user_id = ?'
).all(c.req.param('id'));
return c.json({ posts });
})
.post('/posts', zValidator('json', z.object({
userId: z.string(),
title: z.string(),
content: z.string(),
})), (c) => {
const data = c.req.valid('json');
const post = db.query(
'INSERT INTO posts (id, user_id, title, content) VALUES (?, ?, ?, ?) RETURNING *'
).get(crypto.randomUUID(), data.userId, data.title, data.content);
return c.json({ post }, 201);
});
const client = testClient(app);
// Create user
const userRes = await client.users.$post({
json: { email: 'alice@example.com', name: 'Alice' }
});
expect(userRes.status).toBe(201);
const { user } = await userRes.json();
// Create posts for user
await client.posts.$post({
json: { userId: user.id, title: 'Post 1', content: 'Content 1' }
});
await client.posts.$post({
json: { userId: user.id, title: 'Post 2', content: 'Content 2' }
});
// Get user's posts
const postsRes = await client.users[':id'].posts.$get({
param: { id: user.id }
});
expect(postsRes.status).toBe(200);
const { posts } = await postsRes.json();
expect(posts).toHaveLength(2);
expect(posts.map((p: any) => p.title)).toEqual(['Post 1', 'Post 2']);
});
test('Transaction rollback on error', async () => {
const app = new Hono()
.post('/bulk-create', zValidator('json', z.object({
users: z.array(z.object({ email: z.string(), name: z.string() }))
})), (c) => {
const data = c.req.valid('json');
try {
db.transaction(() => {
for (const user of data.users) {
db.run(
'INSERT INTO users (id, email, name) VALUES (?, ?, ?)',
[crypto.randomUUID(), user.email, user.name]
);
}
})();
return c.json({ created: data.users.length }, 201);
} catch (err) {
throw new HTTPException(400, { message: 'Bulk create failed' });
}
});
const client = testClient(app);
// Attempt to create users with duplicate email
const res = await client['bulk-create'].$post({
json: {
users: [
{ email: 'alice@example.com', name: 'Alice' },
{ email: 'alice@example.com', name: 'Alice Duplicate' }, // Duplicate!
]
}
});
expect(res.status).toBe(400);
// Verify no users were created
const count = db.query('SELECT COUNT(*) as count FROM users').get() as { count: number };
expect(count.count).toBe(0);
});
});
```
## Mocking Patterns
```typescript
describe('Mocking', () => {
test('Mock external API calls', async () => {
// Mock fetch
const originalFetch = global.fetch;
global.fetch = async (url: string | URL | Request) => {
return new Response(JSON.stringify({ mocked: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
const app = new Hono()
.get('/proxy', async (c) => {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return c.json({ data });
});
const client = testClient(app);
const res = await client.proxy.$get();
const data = await res.json();
expect(data.data.mocked).toBe(true);
// Restore
global.fetch = originalFetch;
});
test('Mock database with interface', async () => {
interface IDatabase {
query(sql: string): { get(id: string): any };
}
class MockDatabase implements IDatabase {
private data = new Map([
['1', { id: '1', name: 'Alice' }],
['2', { id: '2', name: 'Bob' }],
]);
query(sql: string) {
return {
get: (id: string) => this.data.get(id),
};
}
}
const mockDb = new MockDatabase();
const app = new Hono()
.get('/users/:id', (c) => {
const user = mockDb.query('SELECT * FROM users WHERE id = ?').get(c.req.param('id'));
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ user });
});
const client = testClient(app);
const res = await client.users[':id'].$get({ param: { id: '1' } });
const data = await res.json();
expect(data.user.name).toBe('Alice');
});
});
```
@@ -0,0 +1,740 @@
# Typed Routes — Complete Examples
Route chaining patterns for full type inference across server and client.
## Basic CRUD API
```typescript
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { Database } from 'bun:sqlite';
// Schemas
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false),
});
const UpdatePostSchema = CreatePostSchema.partial();
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
published: z.enum(['true', 'false']).optional(),
});
// Database setup
const db = new Database('blog.db');
db.run(`
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
published INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Prepared statements
const getAllPosts = db.prepare(`
SELECT * FROM posts
WHERE ($published IS NULL OR published = $published)
ORDER BY created_at DESC
LIMIT $limit OFFSET $offset
`);
const getPostById = db.prepare('SELECT * FROM posts WHERE id = ?');
const insertPost = db.prepare(`
INSERT INTO posts (id, title, content, published)
VALUES (?, ?, ?, ?)
RETURNING *
`);
const updatePost = db.prepare(`
UPDATE posts
SET title = COALESCE(?, title),
content = COALESCE(?, content),
published = COALESCE(?, published),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
RETURNING *
`);
const deletePost = db.prepare('DELETE FROM posts WHERE id = ? RETURNING *');
// ✅ Type-safe routes with chaining
const app = new Hono()
// List posts with pagination and filtering
.get('/posts', zValidator('query', QuerySchema), (c) => {
const { page, limit, published } = c.req.valid('query');
const offset = (page - 1) * limit;
const posts = getAllPosts.all({
$published: published ? (published === 'true' ? 1 : 0) : null,
$limit: limit,
$offset: offset,
});
const total = db.query('SELECT COUNT(*) as count FROM posts').get() as { count: number };
return c.json({
posts,
pagination: {
page,
limit,
total: total.count,
totalPages: Math.ceil(total.count / limit),
},
});
})
// Get single post
.get('/posts/:id', (c) => {
const id = c.req.param('id');
const post = getPostById.get(id);
if (!post) {
throw new HTTPException(404, { message: 'Post not found' });
}
return c.json({ post });
})
// Create post
.post('/posts', zValidator('json', CreatePostSchema), (c) => {
const data = c.req.valid('json');
const post = insertPost.get(
crypto.randomUUID(),
data.title,
data.content,
data.published ? 1 : 0
);
return c.json({ post }, 201);
})
// Update post
.put('/posts/:id', zValidator('json', UpdatePostSchema), (c) => {
const id = c.req.param('id');
const data = c.req.valid('json');
const post = updatePost.get(
data.title || null,
data.content || null,
data.published !== undefined ? (data.published ? 1 : 0) : null,
id
);
if (!post) {
throw new HTTPException(404, { message: 'Post not found' });
}
return c.json({ post });
})
// Patch post (partial update)
.patch('/posts/:id', zValidator('json', UpdatePostSchema), (c) => {
const id = c.req.param('id');
const data = c.req.valid('json');
// Check exists first
const existing = getPostById.get(id);
if (!existing) {
throw new HTTPException(404, { message: 'Post not found' });
}
const post = updatePost.get(
data.title || null,
data.content || null,
data.published !== undefined ? (data.published ? 1 : 0) : null,
id
);
return c.json({ post });
})
// Delete post
.delete('/posts/:id', (c) => {
const id = c.req.param('id');
const post = deletePost.get(id);
if (!post) {
throw new HTTPException(404, { message: 'Post not found' });
}
return c.json({ deleted: true, post });
});
// Export type for RPC client
export type AppType = typeof app;
export default app;
```
## Type-Safe Client Usage
```typescript
// client.ts
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
async function examples() {
// List posts with pagination
const listRes = await client.posts.$get({
query: { page: '1', limit: '10', published: 'true' }
});
const listData = await listRes.json();
// Typed: {
// posts: any[];
// pagination: { page: number; limit: number; total: number; totalPages: number }
// }
console.log(`Found ${listData.posts.length} posts`);
console.log(`Total pages: ${listData.pagination.totalPages}`);
// Get single post
const getRes = await client.posts[':id'].$get({
param: { id: 'post-123' }
});
if (!getRes.ok) {
console.error('Post not found');
return;
}
const getData = await getRes.json();
// Typed: { post: any }
console.log('Post title:', getData.post.title);
// Create post
const createRes = await client.posts.$post({
json: {
title: 'New Post',
content: 'Post content here...',
published: true,
}
});
const createData = await createRes.json();
// Typed: { post: any }
const newPostId = createData.post.id;
// Update post
const updateRes = await client.posts[':id'].$put({
param: { id: newPostId },
json: { title: 'Updated Title' }
});
const updateData = await updateRes.json();
console.log('Updated:', updateData.post.title);
// Delete post
const deleteRes = await client.posts[':id'].$delete({
param: { id: newPostId }
});
const deleteData = await deleteRes.json();
// Typed: { deleted: boolean; post: any }
console.log('Deleted:', deleteData.deleted);
}
```
## Nested Routes with Path Parameters
```typescript
import { Hono } from 'hono';
const app = new Hono()
// User routes
.get('/users/:userId', (c) => {
const userId = c.req.param('userId');
return c.json({ userId, name: 'Alice' });
})
// User's posts
.get('/users/:userId/posts', (c) => {
const userId = c.req.param('userId');
return c.json({ userId, posts: [] });
})
// Specific post for user
.get('/users/:userId/posts/:postId', (c) => {
const { userId, postId } = c.req.param();
// Both typed as string
return c.json({ userId, postId, title: 'Post' });
})
// Post comments
.get('/users/:userId/posts/:postId/comments', (c) => {
const { userId, postId } = c.req.param();
return c.json({ userId, postId, comments: [] });
})
// Specific comment
.get('/users/:userId/posts/:postId/comments/:commentId', (c) => {
const { userId, postId, commentId } = c.req.param();
// All typed as string
return c.json({ userId, postId, commentId, text: 'Comment' });
});
export type AppType = typeof app;
```
### Client Usage for Nested Routes
```typescript
const client = hc<AppType>('http://localhost:3000');
// Access nested routes
const res = await client.users[':userId'].posts[':postId'].comments[':commentId'].$get({
param: {
userId: 'user-123',
postId: 'post-456',
commentId: 'comment-789',
}
});
const data = await res.json();
// Typed: { userId: string; postId: string; commentId: string; text: string }
```
## Complex Query Parameters
```typescript
const SearchSchema = z.object({
q: z.string().min(1),
category: z.enum(['tech', 'business', 'lifestyle']).optional(),
tags: z.array(z.string()).optional(),
minPrice: z.coerce.number().positive().optional(),
maxPrice: z.coerce.number().positive().optional(),
sortBy: z.enum(['price', 'date', 'relevance']).default('relevance'),
order: z.enum(['asc', 'desc']).default('desc'),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
});
const app = new Hono()
.get('/search', zValidator('query', SearchSchema), (c) => {
const query = c.req.valid('query');
// Fully typed with all fields and defaults applied
// Build SQL query dynamically
const conditions: string[] = ['title LIKE ?'];
const params: any[] = [`%${query.q}%`];
if (query.category) {
conditions.push('category = ?');
params.push(query.category);
}
if (query.tags && query.tags.length > 0) {
conditions.push(`tags IN (${query.tags.map(() => '?').join(',')})`);
params.push(...query.tags);
}
if (query.minPrice) {
conditions.push('price >= ?');
params.push(query.minPrice);
}
if (query.maxPrice) {
conditions.push('price <= ?');
params.push(query.maxPrice);
}
const orderClause = `ORDER BY ${query.sortBy} ${query.order.toUpperCase()}`;
const limitClause = `LIMIT ${query.limit} OFFSET ${(query.page - 1) * query.limit}`;
const sql = `
SELECT * FROM products
WHERE ${conditions.join(' AND ')}
${orderClause}
${limitClause}
`;
const products = db.prepare(sql).all(...params);
return c.json({
products,
query: {
q: query.q,
filters: {
category: query.category,
tags: query.tags,
priceRange: {
min: query.minPrice,
max: query.maxPrice,
},
},
sort: { by: query.sortBy, order: query.order },
pagination: { page: query.page, limit: query.limit },
},
});
});
```
### Client with Complex Query
```typescript
const res = await client.search.$get({
query: {
q: 'laptop',
category: 'tech',
tags: ['gaming', 'portable'],
minPrice: '500',
maxPrice: '2000',
sortBy: 'price',
order: 'asc',
page: '2',
limit: '50',
}
});
const data = await res.json();
```
## File Upload with Multipart Form
```typescript
const UploadSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().optional(),
category: z.enum(['image', 'video', 'document']),
});
const app = new Hono()
.post('/upload', zValidator('form', UploadSchema), async (c) => {
const data = c.req.valid('form');
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
throw new HTTPException(400, { message: 'File is required' });
}
// Validate file size (10MB max)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
throw new HTTPException(400, { message: 'File too large (max 10MB)' });
}
// Validate file type
const allowedTypes = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
video: ['video/mp4', 'video/webm'],
document: ['application/pdf', 'application/msword'],
};
if (!allowedTypes[data.category].includes(file.type)) {
throw new HTTPException(400, { message: `Invalid file type for category ${data.category}` });
}
// Generate unique filename
const ext = file.name.split('.').pop();
const filename = `${crypto.randomUUID()}.${ext}`;
const filepath = `./uploads/${data.category}/${filename}`;
// Save file
await Bun.write(filepath, file);
// Save metadata to database
const upload = db.prepare(`
INSERT INTO uploads (id, filename, original_name, title, description, category, size, mime_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *
`).get(
crypto.randomUUID(),
filename,
file.name,
data.title,
data.description || null,
data.category,
file.size,
file.type
);
return c.json({ upload }, 201);
})
.get('/uploads/:id', async (c) => {
const id = c.req.param('id');
const upload = db.prepare('SELECT * FROM uploads WHERE id = ?').get(id);
if (!upload) {
throw new HTTPException(404, { message: 'Upload not found' });
}
const filepath = `./uploads/${upload.category}/${upload.filename}`;
const file = Bun.file(filepath);
if (!(await file.exists())) {
throw new HTTPException(404, { message: 'File not found on disk' });
}
return c.body(file.stream(), {
headers: {
'Content-Type': upload.mime_type,
'Content-Length': upload.size.toString(),
'Content-Disposition': `attachment; filename="${upload.original_name}"`,
},
});
});
```
## Middleware in Route Chain
```typescript
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { HTTPException } from 'hono/http-exception';
// Custom auth middleware
const authMiddleware = async (c: Context, next: Next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) {
throw new HTTPException(401, { message: 'Missing authorization header' });
}
// Verify token (simplified)
const user = await verifyToken(token);
if (!user) {
throw new HTTPException(401, { message: 'Invalid token' });
}
c.set('user', user);
await next();
};
// Role-based access control
const requireRole = (role: 'admin' | 'user') => {
return async (c: Context, next: Next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
if (user.role !== role && user.role !== 'admin') {
throw new HTTPException(403, { message: 'Insufficient permissions' });
}
await next();
};
};
const app = new Hono()
// Global middleware
.use('*', logger())
.use('*', cors())
// Public routes
.get('/health', (c) => c.json({ status: 'ok' }))
.post('/auth/login', loginHandler)
// Protected routes
.use('/api/*', authMiddleware)
.get('/api/profile', (c) => {
const user = c.get('user');
return c.json({ user });
})
// Admin-only routes
.use('/api/admin/*', requireRole('admin'))
.get('/api/admin/users', (c) => {
return c.json({ users: [] });
})
.delete('/api/admin/users/:id', (c) => {
return c.json({ deleted: true });
});
```
## WebSocket Routes
```typescript
import { Hono } from 'hono';
import type { ServerWebSocket } from 'bun';
const app = new Hono()
.get('/ws', (c) => {
const success = c.upgrade();
if (!success) {
return c.text('WebSocket upgrade failed', 400);
}
return undefined; // Upgraded to WebSocket
});
// WebSocket handlers
const websocket = {
open(ws: ServerWebSocket) {
console.log('Client connected');
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
},
message(ws: ServerWebSocket, message: string | Buffer) {
console.log('Received:', message);
try {
const data = JSON.parse(message.toString());
// Echo back
ws.send(JSON.stringify({
type: 'echo',
data,
timestamp: Date.now(),
}));
// Broadcast to all clients
ws.publish('global', JSON.stringify({
type: 'broadcast',
data,
timestamp: Date.now(),
}));
} catch (err) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
}
},
close(ws: ServerWebSocket) {
console.log('Client disconnected');
},
};
Bun.serve({
fetch: app.fetch,
websocket,
port: 3000,
});
```
## Streaming Responses
```typescript
const app = new Hono()
// Server-Sent Events
.get('/events', (c) => {
return c.stream(async (stream) => {
let id = 0;
const interval = setInterval(() => {
stream.writeln(`data: ${JSON.stringify({ id: ++id, timestamp: Date.now() })}\n`);
}, 1000);
// Cleanup on client disconnect
stream.onAbort(() => {
clearInterval(interval);
});
}, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
})
// Large file streaming
.get('/download/:filename', async (c) => {
const filename = c.req.param('filename');
const filepath = `./large-files/${filename}`;
const file = Bun.file(filepath);
if (!(await file.exists())) {
throw new HTTPException(404, { message: 'File not found' });
}
return c.body(file.stream(), {
headers: {
'Content-Type': file.type,
'Content-Length': file.size.toString(),
'Content-Disposition': `attachment; filename="${filename}"`,
},
});
})
// Compressed streaming
.get('/large-data', (c) => {
const data = generateLargeDataset(); // Returns iterator
return c.stream(async (stream) => {
for await (const chunk of data) {
await stream.write(JSON.stringify(chunk) + '\n');
}
}, {
headers: {
'Content-Type': 'application/x-ndjson',
'Content-Encoding': 'gzip',
},
});
});
```
## Type Inference Gotchas
```typescript
// ❌ Type inference breaks
const app = new Hono();
const route1 = app.get('/users', handler);
const route2 = route1.post('/users', handler); // OK so far
app.get('/posts', handler); // Types lost!
export type AppType = typeof app; // Only Hono base type
// ✅ Keep chain intact
const app = new Hono()
.get('/users', handler)
.post('/users', handler)
.get('/posts', handler);
export type AppType = typeof app; // Full route types
// ❌ Conditional routes break chain
const app = new Hono();
if (isDevelopment) {
app.get('/debug', debugHandler); // Types lost
}
app.get('/api/users', handler);
// ✅ Use .use() with conditional
const app = new Hono()
.use('*', (c, next) => {
if (isDevelopment) {
// Add debug routes dynamically
}
return next();
})
.get('/api/users', handler);
// ❌ Variable extraction loses types
const getUserRoute = (app: Hono) => app.get('/users', handler);
const app = new Hono();
getUserRoute(app); // Types lost
// ✅ Return chain from function
const getUserRoute = (app: Hono) => app.get('/users', handler);
const app = getUserRoute(new Hono())
.post('/users', handler);
export type AppType = typeof app;
```