8.2 KiB
8.2 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-hono-middleware-deep | Hono Middleware — modern Express alternative | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Hono Middleware Deep
Modern Express alternative. Edge-friendly (CF Workers / Deno / Bun / Node), TypeScript-first, 작은 bundle.
📖 핵심 개념
- Web standards (Fetch, Request, Response).
- 매 runtime (Node / Bun / Deno / CF / Lambda).
- Type-safe routing.
- Built-in middleware.
💻 코드 패턴
Basic
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.text('Hello'));
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }));
// Cloudflare Worker
export default app;
// Bun
Bun.serve({ fetch: app.fetch });
// Node
import { serve } from '@hono/node-server';
serve(app);
→ 같은 code, 다른 runtime.
Built-in middleware
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
import { compress } from 'hono/compress';
import { etag } from 'hono/etag';
import { jwt } from 'hono/jwt';
app.use(logger());
app.use(cors({ origin: 'https://app.example.com' }));
app.use(secureHeaders());
app.use(compress());
app.use(etag());
app.use('/api/*', jwt({ secret: 'secret' }));
→ 모든 production middleware.
Custom middleware
const auth = async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token) return c.text('Unauthorized', 401);
const user = await verifyToken(token);
if (!user) return c.text('Invalid', 401);
c.set('user', user);
await next();
};
app.use('/api/*', auth);
app.get('/api/me', (c) => {
const user = c.get('user');
return c.json(user);
});
Type-safe context
type Variables = {
user: User;
};
const app = new Hono<{ Variables: Variables }>();
app.use('*', async (c, next) => {
c.set('user', { id: '1' });
await next();
});
app.get('/', (c) => {
const user = c.get('user'); // type: User
return c.json(user);
});
Validator
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
app.post(
'/users',
zValidator('json', z.object({ name: z.string(), email: z.string().email() })),
(c) => {
const data = c.req.valid('json');
// type-safe
return c.json({ id: '...', ...data });
}
);
RPC mode (type-safe client)
// Server
const route = app.post('/users', zValidator('json', userSchema), (c) => {
return c.json({ id: '...' });
});
export type AppType = typeof route;
// Client
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('https://api.example.com');
const r = await client.users.$post({ json: { name: 'Alice', email: 'a@x' } });
const data = await r.json(); // type-safe
→ tRPC 식 type-safe RPC.
Streaming
import { stream, streamSSE } from 'hono/streaming';
app.get('/stream', (c) => {
return stream(c, async (stream) => {
for (let i = 0; i < 100; i++) {
await stream.writeln(`chunk ${i}`);
await stream.sleep(100);
}
});
});
app.get('/sse', (c) => {
return streamSSE(c, async (stream) => {
let id = 0;
while (true) {
await stream.writeSSE({ event: 'msg', data: 'hello', id: String(id++) });
await stream.sleep(1000);
}
});
});
WebSocket (Bun / Deno)
import { upgradeWebSocket } from 'hono/cloudflare-workers';
app.get(
'/ws',
upgradeWebSocket((c) => ({
onMessage: (event, ws) => ws.send(`echo: ${event.data}`),
onClose: () => console.log('closed'),
}))
);
Sub-app (modular)
const userApp = new Hono();
userApp.get('/', (c) => c.json([]));
userApp.get('/:id', (c) => c.json({ id: c.req.param('id') }));
const orderApp = new Hono();
orderApp.get('/', (c) => c.json([]));
const app = new Hono();
app.route('/users', userApp);
app.route('/orders', orderApp);
→ Module 별 분리.
Error handling
app.onError((err, c) => {
console.error(err);
return c.json({ error: err.message }, 500);
});
app.notFound((c) => c.text('Not found', 404));
File serving
import { serveStatic } from '@hono/node-server/serve-static';
app.use('/static/*', serveStatic({ root: './public' }));
OpenAPI integration
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
const app = new OpenAPIHono();
const route = createRoute({
method: 'get',
path: '/users/:id',
request: { params: z.object({ id: z.string() }) },
responses: {
200: { content: { 'application/json': { schema: z.object({ id: z.string() }) } } },
},
});
app.openapi(route, (c) => c.json({ id: c.req.valid('param').id }));
app.doc('/openapi.json', { openapi: '3.0.0', info: { title: 'API', version: '1.0' } });
→ Zod schema → OpenAPI auto.
Performance
Hono: ~30k req/sec on Bun.
Express: ~10k req/sec on Node.
Fastify: ~25k.
→ Hono 가 가장 빠름 + 작은.
Bundle size
Hono: ~12 KB.
Express: ~80 KB.
Fastify: ~90 KB.
→ Edge / serverless 친화.
Use case
- Cloudflare Worker.
- Vercel Edge.
- Bun server.
- Deno server.
- Node 서버 (alternative to Express).
- Lambda.
Production deploy
# Cloudflare
wrangler deploy
# Vercel
vercel --prod
# Bun
bun run server.ts
# Node
node server.js
vs Express
Express:
- 가장 mature.
- 큰 ecosystem.
- Node only.
Hono:
- Modern, edge-friendly.
- Type-safe.
- Multi-runtime.
- 작은 ecosystem (자라는).
→ New project = Hono.
Existing Express = stay.
vs Fastify
Fastify:
- Node-first.
- Plugin system.
- 빠름.
Hono:
- Edge-first.
- Web standards.
- 더 작은.
→ Edge / serverless = Hono.
Node-only enterprise = Fastify.
Middleware order
app.use(logger()); // 1st
app.use(cors());
app.use(jwt({ secret })); // auth
app.use('/api/*', rateLimit); // rate
app.get('/api/users', ...); // route
→ Order matters.
Context (c)
c.req.url // string
c.req.method
c.req.header('...')
c.req.query('...')
c.req.param('...')
c.req.json() // Promise<json>
c.req.formData()
c.req.text()
c.json({}) // Response
c.text('hi')
c.html('<p>hi</p>')
c.redirect('/path')
c.status(404)
c.set('key', val)
c.get('key')
c.env // Cloudflare Worker env
Auth patterns
// JWT
app.use('/api/*', jwt({ secret: c.env.JWT_SECRET }));
// Custom
const apiKey = c.req.header('X-API-Key');
if (apiKey !== c.env.API_KEY) return c.text('Unauthorized', 401);
// Basic auth
import { basicAuth } from 'hono/basic-auth';
app.use('/admin/*', basicAuth({ username: 'admin', password: 'secret' }));
Rate limit
const rateLimiter = new Map<string, { count: number; reset: number }>();
app.use('/api/*', async (c, next) => {
const ip = c.req.header('x-real-ip') ?? 'unknown';
const now = Date.now();
const limit = rateLimiter.get(ip) ?? { count: 0, reset: now + 60000 };
if (now > limit.reset) { limit.count = 0; limit.reset = now + 60000; }
limit.count++;
rateLimiter.set(ip, limit);
if (limit.count > 100) return c.text('Rate limit', 429);
await next();
});
→ Cloudflare = built-in. Self-host = manual.
Real-world
- Cloudflare: Hono 의 큰 user.
- Vercel: Edge functions.
- Several startups: edge-first.
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Edge / Cloudflare | Hono |
| Node 새 project | Hono |
| Existing Express | Stay or migrate gradual |
| Type-safe RPC | Hono RPC mode |
| OpenAPI | Hono + zod-openapi |
| WebSocket | Hono (Bun/Deno) |
❌ 안티패턴
- 모든 거 1 file: sub-app 사용.
- No type-safe: zValidator.
- No error handler: ugly.
- Middleware order 잘못: auth before logger.
- Big bundle: edge limit.
🤖 LLM 활용 힌트
- Hono = modern Express.
- Multi-runtime (CF / Node / Bun / Deno).
- Type-safe RPC + OpenAPI.
- 작은 bundle (edge 친화).