[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
---
|
||||
id: backend-hono-modern
|
||||
title: Hono / Elysia / Modern TS Frameworks
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, hono, elysia, vibe-coding]
|
||||
tech_stack: { language: "TS / Bun / Node", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [Hono, Elysia, Bun.serve, Express alternative, Web Standard, modern TS framework]
|
||||
---
|
||||
|
||||
# Hono / Elysia
|
||||
|
||||
> Express 의 modern 후속. **Web Standard (Request/Response) 기반 + edge runtime 호환 + type-safe**. Hono = universal, Elysia = Bun 친화.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Web Standard: Request / Response (browser native).
|
||||
- Edge: Cloudflare Workers / Deno / Bun.
|
||||
- Type-safe: handler 의 query / body / response 자동 inferred.
|
||||
- 작은 + 빠름.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Hono 기본
|
||||
```ts
|
||||
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') }));
|
||||
|
||||
app.post('/users', async (c) => {
|
||||
const body = await c.req.json();
|
||||
return c.json({ id: '...', ...body }, 201);
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Bun 으로 실행
|
||||
```bash
|
||||
bun run --hot src/index.ts
|
||||
```
|
||||
|
||||
### Cloudflare Workers
|
||||
```ts
|
||||
// wrangler.toml
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
// src/index.ts
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono<{ Bindings: { DB: D1Database; CACHE: KVNamespace } }>();
|
||||
|
||||
app.get('/users/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const cached = await c.env.CACHE.get(`user:${id}`);
|
||||
if (cached) return c.json(JSON.parse(cached));
|
||||
|
||||
const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(id).first();
|
||||
await c.env.CACHE.put(`user:${id}`, JSON.stringify(user), { expirationTtl: 60 });
|
||||
return c.json(user);
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Vercel Edge / Deno
|
||||
```ts
|
||||
import { Hono } from 'hono';
|
||||
const app = new Hono();
|
||||
// ... routes
|
||||
export default app; // 자동 detect
|
||||
```
|
||||
|
||||
### Middleware
|
||||
```ts
|
||||
import { logger } from 'hono/logger';
|
||||
import { cors } from 'hono/cors';
|
||||
import { compress } from 'hono/compress';
|
||||
import { jwt } from 'hono/jwt';
|
||||
|
||||
app.use('*', logger());
|
||||
app.use('*', cors({ origin: 'https://app.com' }));
|
||||
app.use('*', compress());
|
||||
|
||||
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET! }));
|
||||
|
||||
app.get('/api/me', (c) => {
|
||||
const payload = c.get('jwtPayload');
|
||||
return c.json({ user: payload.sub });
|
||||
});
|
||||
```
|
||||
|
||||
### Zod validator
|
||||
```ts
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
|
||||
const CreateUser = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
});
|
||||
|
||||
app.post('/users', zValidator('json', CreateUser), async (c) => {
|
||||
const data = c.req.valid('json'); // typed
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Hono RPC (TS client)
|
||||
```ts
|
||||
// server
|
||||
const route = app.get('/users/:id', (c) => c.json({ id: c.req.param('id'), name: 'A' }));
|
||||
export type AppType = typeof route;
|
||||
|
||||
// client (frontend)
|
||||
import { hc } from 'hono/client';
|
||||
import type { AppType } from '../server';
|
||||
|
||||
const client = hc<AppType>('http://localhost:3000');
|
||||
|
||||
const r = await client.users[':id'].$get({ param: { id: '1' } });
|
||||
const data = await r.json(); // typed!
|
||||
```
|
||||
|
||||
→ tRPC 비슷 — type 가 client / server 공유.
|
||||
|
||||
### Streaming (SSE)
|
||||
```ts
|
||||
import { streamSSE } from 'hono/streaming';
|
||||
|
||||
app.get('/stream', (c) => {
|
||||
return streamSSE(c, async (stream) => {
|
||||
while (true) {
|
||||
await stream.writeSSE({
|
||||
data: JSON.stringify({ time: Date.now() }),
|
||||
event: 'tick',
|
||||
id: crypto.randomUUID(),
|
||||
});
|
||||
await stream.sleep(1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Error handler
|
||||
```ts
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
app.onError((err, c) => {
|
||||
if (err instanceof HTTPException) {
|
||||
return c.json({ error: err.message }, err.status);
|
||||
}
|
||||
return c.json({ error: 'Internal' }, 500);
|
||||
});
|
||||
|
||||
app.get('/protected', (c) => {
|
||||
const auth = c.req.header('Authorization');
|
||||
if (!auth) throw new HTTPException(401, { message: 'Unauthorized' });
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
```
|
||||
|
||||
### Elysia (Bun-only, 매우 빠름)
|
||||
```ts
|
||||
import { Elysia, t } from 'elysia';
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/', () => 'Hello')
|
||||
.get('/users/:id', ({ params: { id } }) => ({ id }))
|
||||
.post('/users', ({ body }) => ({ ...body, id: '...' }), {
|
||||
body: t.Object({
|
||||
email: t.String({ format: 'email' }),
|
||||
name: t.String({ minLength: 1 }),
|
||||
}),
|
||||
})
|
||||
.listen(3000);
|
||||
|
||||
console.log(`http://localhost:${app.server?.port}`);
|
||||
```
|
||||
|
||||
→ TypeBox (JSON Schema) — Bun native.
|
||||
|
||||
### Elysia plugins
|
||||
```ts
|
||||
import { swagger } from '@elysiajs/swagger';
|
||||
import { jwt } from '@elysiajs/jwt';
|
||||
import { cors } from '@elysiajs/cors';
|
||||
|
||||
app
|
||||
.use(swagger()) // /swagger 자동 docs
|
||||
.use(cors())
|
||||
.use(jwt({ secret: process.env.JWT_SECRET }));
|
||||
```
|
||||
|
||||
### Bun.serve (raw, 가장 빠름)
|
||||
```ts
|
||||
Bun.serve({
|
||||
port: 3000,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === '/') return new Response('Hello');
|
||||
if (url.pathname.startsWith('/api/')) return apiHandler(req);
|
||||
return new Response('Not found', { status: 404 });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
→ Framework 없이. 가장 raw.
|
||||
|
||||
### Performance
|
||||
```
|
||||
Bun.serve: > 100K req/s (single core)
|
||||
Elysia: ~80K req/s
|
||||
Hono on Bun: ~80K req/s
|
||||
Hono on Node: ~30K req/s
|
||||
Express: ~10K req/s
|
||||
|
||||
→ 측정 + workload 따라.
|
||||
```
|
||||
|
||||
### File-based routing
|
||||
```
|
||||
Hono = code-first.
|
||||
Elysia = code-first.
|
||||
|
||||
File-based 원하면:
|
||||
- Next.js App Router
|
||||
- Tanstack Start
|
||||
- Astro endpoints
|
||||
- Hono + 자체 file scanner
|
||||
```
|
||||
|
||||
### Database 통합
|
||||
```ts
|
||||
// Hono + Drizzle
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
|
||||
const sql = postgres(process.env.DATABASE_URL!);
|
||||
const db = drizzle(sql);
|
||||
|
||||
app.get('/users/:id', async (c) => {
|
||||
const user = await db.select().from(usersTable).where(eq(usersTable.id, c.req.param('id'))).limit(1);
|
||||
return c.json(user[0]);
|
||||
});
|
||||
```
|
||||
|
||||
### Edge DB combo
|
||||
```
|
||||
Cloudflare Workers + D1: Hono + D1 binding
|
||||
Vercel Edge + Neon: Hono + neon HTTP
|
||||
Bun + Postgres: Hono / Elysia + Bun pg
|
||||
```
|
||||
|
||||
### vs Express
|
||||
```
|
||||
Express:
|
||||
+ 커뮤니티 큼
|
||||
+ 미들웨어 많음
|
||||
- 옛 callback API
|
||||
- Type 약함
|
||||
- Edge 호환 X (Node only)
|
||||
|
||||
Hono / Elysia:
|
||||
+ Modern TS
|
||||
+ Edge runtime
|
||||
+ 빠름
|
||||
+ Type-safe
|
||||
- 작은 ecosystem (커지는 중)
|
||||
```
|
||||
|
||||
### Migration (Express → Hono)
|
||||
```ts
|
||||
// Express
|
||||
app.get('/users/:id', async (req, res) => {
|
||||
res.json(await getUser(req.params.id));
|
||||
});
|
||||
|
||||
// Hono
|
||||
app.get('/users/:id', async (c) => {
|
||||
return c.json(await getUser(c.req.param('id')));
|
||||
});
|
||||
```
|
||||
|
||||
→ 비슷. Migration 가능.
|
||||
|
||||
### Testing (Hono)
|
||||
```ts
|
||||
import { Hono } from 'hono';
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
const app = new Hono();
|
||||
app.get('/', (c) => c.text('Hello'));
|
||||
|
||||
test('GET /', async () => {
|
||||
const res = await app.request('/');
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe('Hello');
|
||||
});
|
||||
```
|
||||
|
||||
→ App.request — 외부 server 필요 X.
|
||||
|
||||
### Deployment options
|
||||
```
|
||||
Hono:
|
||||
- Cloudflare Workers
|
||||
- Vercel Edge
|
||||
- AWS Lambda (with adapter)
|
||||
- Bun
|
||||
- Node
|
||||
- Deno
|
||||
|
||||
Elysia:
|
||||
- Bun (only)
|
||||
```
|
||||
|
||||
### Build / size
|
||||
```
|
||||
Hono: ~12 KB (gzip)
|
||||
Elysia: ~30 KB
|
||||
Express: ~120 KB
|
||||
|
||||
→ Edge runtime 친화.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 환경 | 추천 |
|
||||
|---|---|
|
||||
| Edge runtime | Hono |
|
||||
| Bun max performance | Elysia |
|
||||
| Node + 큰 ecosystem | Hono 또는 Express |
|
||||
| Multi-cloud / portable | Hono |
|
||||
| File-based + full-stack | Next / Tanstack Start |
|
||||
| Raw / 가장 빠른 | Bun.serve |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Express + Edge runtime**: 호환 X.
|
||||
- **Node-specific module on Edge**: 깨짐.
|
||||
- **fetch / Response 의 standard 알기 X**: API confusion.
|
||||
- **Hono RPC + 큰 schema**: 빌드 시간.
|
||||
- **Elysia + Node**: Bun 만.
|
||||
- **Middleware 너무 많이**: latency.
|
||||
- **Type 안 활용**: 의미 없는 framework 선택.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Hono = universal modern.
|
||||
- Elysia = Bun 친화 + 빠름.
|
||||
- Web Standard API (Request / Response).
|
||||
- Hono RPC = type-safe TS fullstack.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_API_Gateway_BFF]]
|
||||
- [[Runtime_Bun_Deno_Comparison]]
|
||||
- [[API_OpenAPI_Spec]]
|
||||
Reference in New Issue
Block a user