@kood/claude-code 0.7.3 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,72 +1,200 @@
1
- # Hono 서버 아키텍처
1
+ # Architecture
2
2
 
3
- > 레이어 기반 아키텍처
3
+ > Hono 애플리케이션 아키텍처
4
+
5
+ <instructions>
6
+ @../guides/conventions.md
7
+ @../guides/routes.md
8
+ @../guides/middleware.md
9
+ </instructions>
10
+
11
+ ---
12
+
13
+ <forbidden>
14
+
15
+ | 분류 | 금지 |
16
+ |------|------|
17
+ | **컨트롤러** | Rails 스타일 클래스 컨트롤러 (`class UserController`) |
18
+ | **타입** | `Context` 직접 타입 지정 (타입 추론 불가) |
19
+ | **RPC** | `c.notFound()` 사용 (클라이언트 타입 추론 불가) |
20
+ | **에러** | 일반 `Error` throw (`HTTPException` 사용) |
21
+ | **검증** | handler 내부 수동 검증 (`zValidator` 사용) |
22
+ | **Barrel Export** | `routes/index.ts`에서 모든 라우트 re-export |
23
+ | **폴더 구조** | `lib/db`, `controllers/` (→ `database/`, `services/` 사용) |
24
+
25
+ </forbidden>
26
+
27
+ ---
28
+
29
+ <required>
30
+
31
+ | 분류 | 필수 |
32
+ |------|------|
33
+ | **핸들러** | 경로 정의 후 직접 작성 (타입 추론 최적화) |
34
+ | **라우트 분리** | `app.route()` 사용 (`routes/users.ts` → `app.route('/users', users)`) |
35
+ | **RPC 타입** | 메서드 체이닝 + `export type AppType = typeof app` |
36
+ | **Env 타입** | `Hono<{ Bindings; Variables }>` 명시 |
37
+ | **검증** | `zValidator('json' \| 'query' \| 'param', schema)` |
38
+ | **인증** | 미들웨어로 분리 (`jwt()`, `bearerAuth()`, 커스텀) |
39
+ | **에러 처리** | `app.onError()` + `HTTPException` |
40
+ | **타입 안전** | TypeScript strict, Zod 스키마 |
41
+
42
+ </required>
4
43
 
5
44
  ---
6
45
 
7
- ## 시스템 개요
46
+ <system_overview>
47
+
48
+ ## System Overview
8
49
 
9
50
  ```
10
- Client → Middleware → Routes → Validation → Services → Database
51
+ ┌─────────────────────────────────────────────────────────────────┐
52
+ │ Client │
53
+ │ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
54
+ │ │ hc (RPC) │───▶│ fetch/axios │───▶│ React/Vue │ │
55
+ │ │ Type-safe │◀───│ HTTP Client │◀───│ Frontend │ │
56
+ │ └────────────────┘ └───────┬────────┘ └───────────────┘ │
57
+ └────────────────────────────────┼─────────────────────────────────┘
58
+
59
+ ┌─────────────────────────────────────────────────────────────────┐
60
+ │ Hono Server │
61
+ │ ┌────────────────────────────────────────────────────────────┐ │
62
+ │ │ Middleware: Logger | CORS | Auth │ │
63
+ │ └────────────────────────────┬───────────────────────────────┘ │
64
+ │ ┌────────────────────────────▼───────────────────────────────┐ │
65
+ │ │ Routes (Handlers) │ │
66
+ │ │ /api/users | /api/posts | /api/auth │ │
67
+ │ └────────────────────────────┬───────────────────────────────┘ │
68
+ │ ┌────────────────────────────▼───────────────────────────────┐ │
69
+ │ │ Validation: Zod + zValidator │ │
70
+ │ └────────────────────────────┬───────────────────────────────┘ │
71
+ │ ┌────────────────────────────▼───────────────────────────────┐ │
72
+ │ │ Services Layer │ │
73
+ │ │ queries.ts (조회) | mutations.ts (생성/수정/삭제) │ │
74
+ │ └────────────────────────────┬───────────────────────────────┘ │
75
+ │ ┌────────────────────────────▼───────────────────────────────┐ │
76
+ │ │ Error Handling: HTTPException │ │
77
+ │ └────────────────────────────┬───────────────────────────────┘ │
78
+ └───────────────────────────────┼──────────────────────────────────┘
79
+
80
+ ┌─────────────────────────────────────────────────────────────────┐
81
+ │ Database Layer │
82
+ │ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
83
+ │ │ Prisma Client │───▶│ PostgreSQL │ │ Redis │ │
84
+ │ └────────────────┘ └────────────────┘ └───────────────┘ │
85
+ └─────────────────────────────────────────────────────────────────┘
11
86
  ```
12
87
 
88
+ </system_overview>
89
+
13
90
  ---
14
91
 
15
- ## 프로젝트 구조
92
+ <folder_structure>
93
+
94
+ ## Folder Structure
16
95
 
17
96
  ```
18
97
  src/
19
- ├── index.ts # Entry point
20
- ├── routes/ # 라우트 모듈
21
- ├── middleware/ # 커스텀 미들웨어
22
- ├── validators/ # Zod 스키마
23
- ├── services/ # 비즈니스 로직
98
+ ├── index.ts # 엔트리포인트, 앱 구성
99
+ ├── routes/ # 라우트 모듈
100
+ ├── users.ts # /api/users/*
101
+ ├── posts.ts # /api/posts/*
102
+ │ └── auth.ts # /api/auth/*
103
+ ├── middleware/ # 커스텀 미들웨어
104
+ │ ├── auth.ts # JWT/Bearer 인증
105
+ │ ├── logging.ts # 요청 로깅
106
+ │ └── error.ts # 전역 에러 핸들러
107
+ ├── validators/ # Zod 스키마
108
+ │ ├── user.ts
109
+ │ ├── post.ts
110
+ │ └── common.ts # 공통 스키마 (pagination 등)
111
+ ├── services/ # 비즈니스 로직
24
112
  │ └── user/
25
- │ ├── queries.ts # 조회
26
- │ └── mutations.ts # 생성/수정/삭제
27
- ├── database/ # Prisma Client
28
- ├── types/ # 타입 정의
29
- └── lib/ # 유틸리티
113
+ │ ├── queries.ts # 조회 로직
114
+ │ └── mutations.ts # 생성/수정/삭제 로직
115
+ ├── database/ # Prisma Client
116
+ │ └── prisma.ts
117
+ ├── types/ # 타입 정의
118
+ │ └── env.ts # Bindings, Variables
119
+ ├── errors/ # 커스텀 에러 클래스
120
+ │ └── domain.ts
121
+ └── lib/ # 유틸리티
122
+ └── utils.ts
30
123
  ```
31
124
 
32
- ---
125
+ ### 폴더 역할
33
126
 
34
- ## Entry Point
127
+ | 폴더 | 역할 | 예시 |
128
+ |------|------|------|
129
+ | **routes/** | HTTP 라우팅, 핸들러 | users.ts, posts.ts |
130
+ | **middleware/** | 요청/응답 처리 | auth.ts, logging.ts |
131
+ | **validators/** | Zod 스키마 정의 | user.ts, post.ts |
132
+ | **services/** | 비즈니스 로직, DB 쿼리 | queries.ts, mutations.ts |
133
+ | **database/** | Prisma 싱글톤 | prisma.ts |
134
+ | **types/** | TypeScript 타입 | env.ts (Bindings, Variables) |
135
+ | **errors/** | 커스텀 에러 클래스 | DomainError, NotFoundError |
35
136
 
36
- ```typescript
37
- // src/index.ts
38
- import { Hono } from 'hono'
39
- import { logger } from 'hono/logger'
40
- import { cors } from 'hono/cors'
41
- import { HTTPException } from 'hono/http-exception'
42
- import { users } from './routes/users'
137
+ </folder_structure>
43
138
 
44
- type Bindings = { DATABASE_URL: string; JWT_SECRET: string }
45
- type Variables = { userId: string }
139
+ ---
46
140
 
47
- const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
141
+ <folder_structure_advanced>
48
142
 
49
- app.use(logger())
50
- app.use('/api/*', cors())
143
+ ## Clean Architecture (대규모)
51
144
 
52
- app.onError((err, c) => {
53
- if (err instanceof HTTPException) {
54
- return c.json({ error: err.message }, err.status)
55
- }
56
- return c.json({ error: 'Internal Server Error' }, 500)
57
- })
145
+ > ⚠️ 대규모 프로젝트에서만 권장. 소규모는 위 기본 구조 사용.
58
146
 
59
- app.notFound((c) => c.json({ error: 'Not Found' }, 404))
147
+ ```
148
+ src/
149
+ ├── domain/ # 순수 비즈니스 로직
150
+ │ ├── entities/ # DTOs, Value Objects
151
+ │ ├── repositories/ # 저장소 인터페이스 (추상)
152
+ │ ├── schemas/ # Zod 검증 스키마
153
+ │ └── use-cases/ # 비즈니스 규칙
154
+ ├── infrastructure/ # 외부 시스템 연동
155
+ │ ├── database/ # DB 연결, 마이그레이션
156
+ │ ├── repositories/ # 저장소 구현체
157
+ │ └── external/ # 외부 API 클라이언트
158
+ ├── presentation/ # HTTP 계층
159
+ │ ├── routes/ # 라우트 정의
160
+ │ ├── handlers/ # HTTP 핸들러
161
+ │ └── middlewares/ # 인증, 로깅, 에러
162
+ ├── shared/ # 공유 유틸
163
+ │ ├── errors/ # 커스텀 에러
164
+ │ ├── utils/ # 유틸 함수
165
+ │ └── types/ # 공유 타입
166
+ └── index.ts # 엔트리포인트
167
+ ```
60
168
 
61
- app.route('/api/users', users)
62
- app.get('/health', (c) => c.json({ status: 'ok' }))
169
+ ### 의존성 방향
63
170
 
64
- export default app
65
171
  ```
172
+ Domain ← Application ← Presentation ← Infrastructure
173
+ (내부) (외부)
174
+ ```
175
+
176
+ | 프로젝트 규모 | 권장 구조 |
177
+ |--------------|----------|
178
+ | **소규모** | 기본 구조 (routes + services + database) |
179
+ | **중규모** | 3계층 (presentation + services + infrastructure) |
180
+ | **대규모** | Clean Architecture 4-5계층 |
181
+
182
+ </folder_structure_advanced>
66
183
 
67
184
  ---
68
185
 
69
- ## Routes Layer
186
+ <layers>
187
+
188
+ ## Layer Architecture
189
+
190
+ ### 1. Routes Layer
191
+
192
+ > ⚠️ **핸들러 직접 작성 필수** (타입 추론 최적화)
193
+ >
194
+ > | ❌ 금지 | ✅ 필수 |
195
+ > |--------|--------|
196
+ > | `class UserController` | `app.get('/users', (c) => ...)` |
197
+ > | `app.get('/users', controller.getUser)` | `app.route('/users', usersRoute)` |
70
198
 
71
199
  ```typescript
72
200
  // routes/users.ts
@@ -74,120 +202,250 @@ import { Hono } from 'hono'
74
202
  import { zValidator } from '@hono/zod-validator'
75
203
  import { authMiddleware } from '@/middleware/auth'
76
204
  import { createUserSchema, userIdSchema } from '@/validators/user'
77
- import { getUsers, createUser } from '@/services/user'
205
+ import { getUsers, getUserById, createUser } from '@/services/user'
78
206
 
79
207
  const users = new Hono()
80
208
 
81
- users.get('/', zValidator('query', paginationSchema), async (c) => {
82
- const { page, limit } = c.req.valid('query')
83
- return c.json(await getUsers({ page, limit }))
209
+ // 목록 조회
210
+ users.get('/', async (c) => {
211
+ const result = await getUsers()
212
+ return c.json(result)
84
213
  })
85
214
 
86
- users.post('/', authMiddleware, zValidator('json', createUserSchema), async (c) => {
87
- const data = c.req.valid('json')
88
- return c.json({ user: await createUser(data) }, 201)
215
+ // 단건 조회
216
+ users.get('/:id', zValidator('param', userIdSchema), async (c) => {
217
+ const { id } = c.req.valid('param')
218
+ const user = await getUserById(id)
219
+ return c.json(user)
89
220
  })
90
221
 
222
+ // 생성 (인증 + 검증)
223
+ users.post(
224
+ '/',
225
+ authMiddleware,
226
+ zValidator('json', createUserSchema),
227
+ async (c) => {
228
+ const data = c.req.valid('json')
229
+ const user = await createUser(data)
230
+ return c.json({ user }, 201)
231
+ }
232
+ )
233
+
91
234
  export { users }
92
235
  ```
93
236
 
94
- ---
237
+ ### RPC 타입 안전성
238
+
239
+ > 메서드 체이닝으로 타입 내보내기
240
+
241
+ ```typescript
242
+ // routes/users.ts (RPC 버전)
243
+ const users = new Hono()
244
+ .get('/', listUsers)
245
+ .get('/:id', getUser)
246
+ .post('/', createUser)
247
+ .put('/:id', updateUser)
248
+ .delete('/:id', deleteUser)
249
+
250
+ export { users }
251
+ export type UsersType = typeof users
252
+
253
+ // index.ts
254
+ const app = new Hono()
255
+ .route('/api/users', users)
256
+ .route('/api/posts', posts)
257
+
258
+ export type AppType = typeof app
95
259
 
96
- ## Middleware Layer
260
+ // client
261
+ import { hc } from 'hono/client'
262
+ import type { AppType } from './server'
263
+
264
+ const client = hc<AppType>('http://localhost:3000')
265
+ const res = await client.api.users.$get() // 완전한 타입 추론
266
+ ```
267
+
268
+ ### 2. Middleware Layer
269
+
270
+ > 양파 구조 (Onion): 요청 → 미들웨어1 → 미들웨어2 → 핸들러 → 미들웨어2 → 미들웨어1 → 응답
97
271
 
98
272
  ```typescript
99
273
  // middleware/auth.ts
100
274
  import { createMiddleware } from 'hono/factory'
101
275
  import { HTTPException } from 'hono/http-exception'
102
276
 
103
- type Env = {
277
+ type AuthEnv = {
104
278
  Bindings: { JWT_SECRET: string }
105
- Variables: { userId: string }
279
+ Variables: { user: { id: string; role: string } }
106
280
  }
107
281
 
108
- export const authMiddleware = createMiddleware<Env>(async (c, next) => {
282
+ export const authMiddleware = createMiddleware<AuthEnv>(async (c, next) => {
109
283
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
110
- if (!token) throw new HTTPException(401, { message: 'Unauthorized' })
111
284
 
112
- const payload = await verifyToken(token, c.env.JWT_SECRET)
113
- c.set('userId', payload.sub)
114
- await next()
285
+ if (!token) {
286
+ throw new HTTPException(401, { message: 'Unauthorized' })
287
+ }
288
+
289
+ try {
290
+ const user = await verifyToken(token, c.env.JWT_SECRET)
291
+ c.set('user', user)
292
+ await next()
293
+ } catch {
294
+ throw new HTTPException(401, { message: 'Invalid token' })
295
+ }
115
296
  })
297
+
298
+ // 역할 기반 접근 제어
299
+ export const requireRole = (allowedRoles: string[]) => {
300
+ return createMiddleware<AuthEnv>(async (c, next) => {
301
+ const user = c.get('user')
302
+
303
+ if (!allowedRoles.includes(user.role)) {
304
+ throw new HTTPException(403, { message: 'Forbidden' })
305
+ }
306
+
307
+ await next()
308
+ })
309
+ }
116
310
  ```
117
311
 
118
- ---
312
+ #### 빌트인 미들웨어
313
+
314
+ | 카테고리 | 미들웨어 | 용도 |
315
+ |----------|----------|------|
316
+ | **인증** | `jwt`, `bearerAuth`, `basicAuth` | 토큰/자격증명 검증 |
317
+ | **성능** | `cache`, `compress`, `etag`, `timeout` | 캐싱, 압축 |
318
+ | **보안** | `cors`, `csrf`, `secureHeaders` | 보안 헤더, CORS |
319
+ | **유틸** | `logger`, `bodyLimit`, `requestId` | 로깅, 제한 |
320
+
321
+ ### 3. Validators Layer
119
322
 
120
- ## Validators Layer
323
+ > Zod v4 + @hono/zod-validator
121
324
 
122
325
  ```typescript
123
326
  // validators/user.ts
124
327
  import { z } from 'zod'
125
328
 
329
+ // 생성 스키마
126
330
  export const createUserSchema = z.object({
127
- email: z.email(), // Zod v4
331
+ email: z.email(), // Zod v4
128
332
  name: z.string().min(1).trim(),
129
333
  password: z.string().min(8),
130
334
  })
131
335
 
336
+ // 수정 스키마
337
+ export const updateUserSchema = createUserSchema.partial()
338
+
339
+ // 파라미터 스키마
340
+ export const userIdSchema = z.object({
341
+ id: z.string().uuid(),
342
+ })
343
+
344
+ // 쿼리 스키마
132
345
  export const paginationSchema = z.object({
133
346
  page: z.coerce.number().positive().default(1),
134
347
  limit: z.coerce.number().max(100).default(10),
135
348
  })
136
349
 
350
+ // 타입 내보내기
137
351
  export type CreateUserInput = z.infer<typeof createUserSchema>
352
+ export type UpdateUserInput = z.infer<typeof updateUserSchema>
138
353
  ```
139
354
 
140
- ---
355
+ #### 다중 Validator 조합
141
356
 
142
- ## Services Layer
357
+ ```typescript
358
+ app.put(
359
+ '/users/:id',
360
+ zValidator('param', userIdSchema),
361
+ zValidator('query', paginationSchema),
362
+ zValidator('json', updateUserSchema),
363
+ async (c) => {
364
+ const { id } = c.req.valid('param')
365
+ const query = c.req.valid('query')
366
+ const body = c.req.valid('json')
367
+ // 모든 입력값 타입 안전
368
+ }
369
+ )
370
+ ```
371
+
372
+ ### 4. Services Layer
143
373
 
144
- ### Queries
374
+ > 비즈니스 로직, Prisma 쿼리
145
375
 
146
376
  ```typescript
147
377
  // services/user/queries.ts
148
378
  import { HTTPException } from 'hono/http-exception'
149
379
  import { prisma } from '@/database/prisma'
150
380
 
151
- export const getUsers = async ({ page, limit }) => {
381
+ export const getUsers = async ({ page = 1, limit = 10 }) => {
152
382
  const [users, total] = await Promise.all([
153
383
  prisma.user.findMany({
154
384
  skip: (page - 1) * limit,
155
385
  take: limit,
156
- select: { id: true, email: true, name: true },
386
+ select: { id: true, email: true, name: true, createdAt: true },
157
387
  }),
158
388
  prisma.user.count(),
159
389
  ])
160
- return { users, pagination: { page, limit, total } }
390
+
391
+ return {
392
+ users,
393
+ pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
394
+ }
161
395
  }
162
396
 
163
397
  export const getUserById = async (id: string) => {
164
- const user = await prisma.user.findUnique({ where: { id } })
165
- if (!user) throw new HTTPException(404, { message: 'User not found' })
398
+ const user = await prisma.user.findUnique({
399
+ where: { id },
400
+ select: { id: true, email: true, name: true, createdAt: true },
401
+ })
402
+
403
+ if (!user) {
404
+ throw new HTTPException(404, { message: 'User not found' })
405
+ }
406
+
166
407
  return user
167
408
  }
168
409
  ```
169
410
 
170
- ### Mutations
171
-
172
411
  ```typescript
173
412
  // services/user/mutations.ts
174
413
  import { HTTPException } from 'hono/http-exception'
175
414
  import { prisma } from '@/database/prisma'
415
+ import type { CreateUserInput } from '@/validators/user'
176
416
 
177
417
  export const createUser = async (data: CreateUserInput) => {
178
- const existing = await prisma.user.findUnique({ where: { email: data.email } })
179
- if (existing) throw new HTTPException(409, { message: 'Email already exists' })
418
+ const existing = await prisma.user.findUnique({
419
+ where: { email: data.email },
420
+ })
421
+
422
+ if (existing) {
423
+ throw new HTTPException(409, { message: 'Email already exists' })
424
+ }
180
425
 
181
426
  return prisma.user.create({
182
- data: { ...data, password: await hashPassword(data.password) },
427
+ data: {
428
+ ...data,
429
+ password: await hashPassword(data.password),
430
+ },
183
431
  select: { id: true, email: true, name: true },
184
432
  })
185
433
  }
186
- ```
187
434
 
188
- ---
435
+ export const updateUser = async (id: string, data: Partial<CreateUserInput>) => {
436
+ return prisma.user.update({
437
+ where: { id },
438
+ data,
439
+ select: { id: true, email: true, name: true },
440
+ })
441
+ }
442
+
443
+ export const deleteUser = async (id: string) => {
444
+ return prisma.user.delete({ where: { id } })
445
+ }
446
+ ```
189
447
 
190
- ## Database Layer
448
+ ### 5. Database Layer
191
449
 
192
450
  ```typescript
193
451
  // database/prisma.ts
@@ -202,54 +460,457 @@ if (process.env.NODE_ENV !== 'production') {
202
460
  }
203
461
  ```
204
462
 
463
+ </layers>
464
+
465
+ ---
466
+
467
+ <data_flow>
468
+
469
+ ## Data Flow
470
+
471
+ ### Request Flow (요청)
472
+
473
+ ```
474
+ Client → Middleware (Logger → CORS → Auth) → Route → zValidator → Handler → Service → Prisma → Database
475
+ ```
476
+
477
+ ```typescript
478
+ // 1. Client 요청
479
+ const res = await client.api.users.$post({
480
+ json: { email: 'user@example.com', name: 'User', password: '12345678' }
481
+ })
482
+
483
+ // 2. 미들웨어 체인
484
+ app.use(logger()) // 로깅
485
+ app.use('/api/*', cors()) // CORS
486
+ app.use('/api/*', authMiddleware) // 인증
487
+
488
+ // 3. 라우트 + 검증 + 핸들러
489
+ app.post('/api/users',
490
+ zValidator('json', createUserSchema), // 검증
491
+ async (c) => {
492
+ const data = c.req.valid('json')
493
+ const user = await createUser(data) // 서비스 호출
494
+ return c.json({ user }, 201)
495
+ }
496
+ )
497
+
498
+ // 4. 서비스 → DB
499
+ export const createUser = async (data) => {
500
+ return prisma.user.create({ data })
501
+ }
502
+ ```
503
+
504
+ ### Response Flow (응답)
505
+
506
+ ```
507
+ Database → Prisma → Service → Handler → (Error Handler?) → Middleware → Client
508
+ ```
509
+
510
+ ```typescript
511
+ // 정상 응답
512
+ return c.json({ user }, 201)
513
+
514
+ // 에러 응답 (HTTPException → onError)
515
+ throw new HTTPException(404, { message: 'User not found' })
516
+
517
+ // 전역 에러 핸들러
518
+ app.onError((err, c) => {
519
+ if (err instanceof HTTPException) {
520
+ return c.json({ error: err.message }, err.status)
521
+ }
522
+ console.error(err)
523
+ return c.json({ error: 'Internal Server Error' }, 500)
524
+ })
525
+ ```
526
+
527
+ </data_flow>
528
+
529
+ ---
530
+
531
+ <error_handling>
532
+
533
+ ## Error Handling
534
+
535
+ ### 전역 에러 핸들러
536
+
537
+ ```typescript
538
+ // index.ts
539
+ import { Hono } from 'hono'
540
+ import { HTTPException } from 'hono/http-exception'
541
+
542
+ const app = new Hono()
543
+
544
+ // 전역 에러 핸들러
545
+ app.onError((err, c) => {
546
+ console.error(`[Error] ${err.message}`, err.stack)
547
+
548
+ if (err instanceof HTTPException) {
549
+ return c.json({
550
+ success: false,
551
+ error: err.message,
552
+ ...(err.cause && { details: err.cause }),
553
+ }, err.status)
554
+ }
555
+
556
+ return c.json({
557
+ success: false,
558
+ error: 'Internal Server Error',
559
+ }, 500)
560
+ })
561
+
562
+ // 404 핸들러
563
+ app.notFound((c) => {
564
+ return c.json({
565
+ success: false,
566
+ error: 'Not Found',
567
+ }, 404)
568
+ })
569
+ ```
570
+
571
+ ### HTTPException 사용
572
+
573
+ | 상태 코드 | 용도 | 예시 |
574
+ |----------|------|------|
575
+ | **400** | 잘못된 요청 | 검증 실패 |
576
+ | **401** | 인증 필요 | 토큰 없음/만료 |
577
+ | **403** | 권한 없음 | 역할 부족 |
578
+ | **404** | 리소스 없음 | 사용자 없음 |
579
+ | **409** | 충돌 | 이메일 중복 |
580
+ | **500** | 서버 오류 | 예외 처리 안됨 |
581
+
582
+ ```typescript
583
+ import { HTTPException } from 'hono/http-exception'
584
+
585
+ // 기본 사용
586
+ throw new HTTPException(404, { message: 'User not found' })
587
+
588
+ // 상세 정보 포함
589
+ throw new HTTPException(400, {
590
+ message: 'Validation failed',
591
+ cause: { field: 'email', error: 'Invalid format' },
592
+ })
593
+ ```
594
+
595
+ ### 커스텀 에러 클래스 (선택)
596
+
597
+ ```typescript
598
+ // errors/domain.ts
599
+ export class DomainError extends Error {
600
+ constructor(
601
+ message: string,
602
+ public readonly code: string,
603
+ public readonly statusCode: number = 400
604
+ ) {
605
+ super(message)
606
+ this.name = 'DomainError'
607
+ }
608
+ }
609
+
610
+ export class NotFoundError extends DomainError {
611
+ constructor(resource: string) {
612
+ super(`${resource} not found`, 'NOT_FOUND', 404)
613
+ }
614
+ }
615
+
616
+ // 사용
617
+ throw new NotFoundError('User')
618
+ ```
619
+
620
+ </error_handling>
621
+
205
622
  ---
206
623
 
624
+ <context_management>
625
+
626
+ ## Context & Type Management
627
+
628
+ ### Env 타입 정의
629
+
630
+ ```typescript
631
+ // types/env.ts
632
+ export type Bindings = {
633
+ DATABASE_URL: string
634
+ JWT_SECRET: string
635
+ REDIS_URL?: string
636
+ }
637
+
638
+ export type Variables = {
639
+ user: { id: string; email: string; role: string }
640
+ requestId: string
641
+ }
642
+
643
+ export type AppEnv = {
644
+ Bindings: Bindings
645
+ Variables: Variables
646
+ }
647
+ ```
648
+
649
+ ### 앱에 타입 적용
650
+
651
+ ```typescript
652
+ // index.ts
653
+ import { Hono } from 'hono'
654
+ import type { AppEnv } from '@/types/env'
655
+
656
+ const app = new Hono<AppEnv>()
657
+
658
+ // Bindings 접근
659
+ app.get('/config', (c) => {
660
+ const dbUrl = c.env.DATABASE_URL // 타입 안전
661
+ return c.json({ configured: !!dbUrl })
662
+ })
663
+
664
+ // Variables 접근
665
+ app.get('/me', authMiddleware, (c) => {
666
+ const user = c.get('user') // 타입 안전
667
+ return c.json(user)
668
+ })
669
+ ```
670
+
671
+ ### Factory로 타입 중앙화
672
+
673
+ ```typescript
674
+ // lib/factory.ts
675
+ import { createFactory } from 'hono/factory'
676
+ import type { AppEnv } from '@/types/env'
677
+
678
+ export const factory = createFactory<AppEnv>({
679
+ initApp: (app) => {
680
+ app.use(logger())
681
+ app.use(requestId())
682
+ },
683
+ })
684
+
685
+ // 다른 파일에서 사용
686
+ const app = factory.createApp()
687
+ const middleware = factory.createMiddleware(...)
688
+ const handlers = factory.createHandlers(...)
689
+ ```
690
+
691
+ </context_management>
692
+
693
+ ---
694
+
695
+ <rpc_pattern>
696
+
207
697
  ## RPC Pattern
208
698
 
699
+ ### 서버 설정
700
+
209
701
  ```typescript
210
- // server
702
+ // server.ts
703
+ import { Hono } from 'hono'
704
+ import { zValidator } from '@hono/zod-validator'
705
+ import { createPostSchema } from '@/validators/post'
706
+
211
707
  const app = new Hono()
212
- .route('/api/users', users)
708
+ .get('/posts', async (c) => {
709
+ const posts = await getPosts()
710
+ return c.json(posts)
711
+ })
712
+ .post('/posts', zValidator('json', createPostSchema), async (c) => {
713
+ const data = c.req.valid('json')
714
+ const post = await createPost(data)
715
+ return c.json(post, 201)
716
+ })
717
+ .get('/posts/:id', async (c) => {
718
+ const id = c.req.param('id')
719
+ const post = await getPostById(id)
720
+ return c.json(post)
721
+ })
213
722
 
214
723
  export type AppType = typeof app
724
+ export default app
725
+ ```
215
726
 
216
- // client
727
+ ### 클라이언트 사용
728
+
729
+ ```typescript
730
+ // client.ts
217
731
  import { hc } from 'hono/client'
218
732
  import type { AppType } from './server'
219
733
 
220
- const client = hc<AppType>('http://localhost:8787')
734
+ const client = hc<AppType>('http://localhost:3000')
735
+
736
+ // GET 요청
737
+ const posts = await client.posts.$get()
738
+ const data = await posts.json()
739
+
740
+ // POST 요청 (타입 안전)
741
+ const newPost = await client.posts.$post({
742
+ json: { title: 'Hello', content: 'World' },
743
+ })
221
744
 
222
- const res = await client.api.users.$get({ query: { page: '1' } })
223
- const data = await res.json()
745
+ // 파라미터
746
+ const post = await client.posts[':id'].$get({
747
+ param: { id: '123' },
748
+ })
224
749
  ```
225
750
 
751
+ </rpc_pattern>
752
+
226
753
  ---
227
754
 
228
- ## Tech Stack
755
+ <quick_patterns>
229
756
 
230
- | 분류 | 기술 | 버전 |
231
- |------|------|------|
757
+ ## Quick Patterns
758
+
759
+ ```typescript
760
+ // 앱 기본 구조
761
+ import { Hono } from 'hono'
762
+ import { logger } from 'hono/logger'
763
+ import { cors } from 'hono/cors'
764
+ import { HTTPException } from 'hono/http-exception'
765
+
766
+ type Bindings = { DATABASE_URL: string; JWT_SECRET: string }
767
+ type Variables = { userId: string }
768
+
769
+ const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
770
+
771
+ app.use(logger())
772
+ app.use('/api/*', cors())
773
+
774
+ app.onError((err, c) => {
775
+ if (err instanceof HTTPException) {
776
+ return c.json({ error: err.message }, err.status)
777
+ }
778
+ return c.json({ error: 'Internal Server Error' }, 500)
779
+ })
780
+
781
+ app.notFound((c) => c.json({ error: 'Not Found' }, 404))
782
+
783
+ export default app
784
+
785
+
786
+ // Zod v4 스키마
787
+ import { z } from 'zod'
788
+
789
+ const createUserSchema = z.object({
790
+ email: z.email(),
791
+ name: z.string().min(1).trim(),
792
+ website: z.url().optional(),
793
+ })
794
+
795
+ const paginationSchema = z.object({
796
+ page: z.coerce.number().positive().default(1),
797
+ limit: z.coerce.number().max(100).default(10),
798
+ })
799
+
800
+
801
+ // 라우트 + 검증
802
+ import { zValidator } from '@hono/zod-validator'
803
+
804
+ app.post('/users', zValidator('json', createUserSchema), async (c) => {
805
+ const data = c.req.valid('json')
806
+ return c.json({ user: await createUser(data) }, 201)
807
+ })
808
+
809
+
810
+ // 커스텀 미들웨어
811
+ import { createMiddleware } from 'hono/factory'
812
+
813
+ const authMiddleware = createMiddleware<Env>(async (c, next) => {
814
+ const token = c.req.header('Authorization')?.replace('Bearer ', '')
815
+ if (!token) throw new HTTPException(401, { message: 'Unauthorized' })
816
+ c.set('userId', await verifyToken(token))
817
+ await next()
818
+ })
819
+
820
+ app.use('/api/*', authMiddleware)
821
+
822
+
823
+ // 라우트 분리 + 통합
824
+ // routes/users.ts
825
+ const users = new Hono()
826
+ .get('/', listUsers)
827
+ .post('/', createUser)
828
+ .get('/:id', getUser)
829
+
830
+ export { users }
831
+
832
+ // index.ts
833
+ app.route('/api/users', users)
834
+ app.route('/api/posts', posts)
835
+
836
+
837
+ // RPC 타입 내보내기
838
+ const app = new Hono()
839
+ .route('/api/users', users)
840
+ .route('/api/posts', posts)
841
+
842
+ export type AppType = typeof app
843
+ ```
844
+
845
+ </quick_patterns>
846
+
847
+ ---
848
+
849
+ <routers>
850
+
851
+ ## Router Types
852
+
853
+ | 라우터 | 특성 | 사용 케이스 |
854
+ |--------|------|-------------|
855
+ | **SmartRouter** (기본) | RegExp + Trie 자동 선택 | 대부분의 경우 |
856
+ | **RegExpRouter** | 단일 정규식, 최고 속도 | 고성능 필요 시 |
857
+ | **TrieRouter** | 모든 패턴 지원 | 복잡한 라우팅 |
858
+ | **LinearRouter** | 빠른 초기화 | Serverless, Edge |
859
+ | **PatternRouter** | 최소 번들 (15KB 미만) | 번들 크기 중요 시 |
860
+
861
+ ```typescript
862
+ import { Hono } from 'hono'
863
+ import { RegExpRouter } from 'hono/router/reg-exp-router'
864
+
865
+ const app = new Hono({ router: new RegExpRouter() })
866
+ ```
867
+
868
+ </routers>
869
+
870
+ ---
871
+
872
+ <tech_stack>
873
+
874
+ ## Technology Stack
875
+
876
+ | Layer | Technology | Version |
877
+ |-------|------------|---------|
232
878
  | Framework | Hono | latest |
233
879
  | Validation | Zod | **4.x** |
234
880
  | ORM | Prisma | **7.x** |
235
- | Runtime | Cloudflare Workers, Node.js, Bun | - |
881
+ | Runtime | Cloudflare Workers, Bun, Node.js, Deno | - |
882
+ | Auth | JWT, Bearer Auth | built-in |
883
+ | Testing | Vitest | latest |
884
+
885
+ </tech_stack>
236
886
 
237
887
  ---
238
888
 
239
- ## 패턴 요약
889
+ <deployment>
240
890
 
241
- | 레이어 | 역할 | 파일 |
242
- |--------|------|------|
243
- | Routes | HTTP 라우팅 | `routes/*.ts` |
244
- | Middleware | 요청/응답 처리 | `middleware/*.ts` |
245
- | Validators | 입력 검증 | `validators/*.ts` |
246
- | Services | 비즈니스 로직 | `services/*/*.ts` |
247
- | Database | 데이터 액세스 | `database/prisma.ts` |
891
+ ## Deployment Targets
248
892
 
249
- ---
893
+ | 환경 | 특징 | 설정 |
894
+ |------|------|------|
895
+ | **Cloudflare Workers** | Edge, 초저지연 | `wrangler.toml` |
896
+ | **Bun** | 고성능 런타임 | `bun run` |
897
+ | **Node.js** | 호환성 최고 | `@hono/node-server` |
898
+ | **Deno** | 보안, 내장 TS | `deno serve` |
899
+ | **AWS Lambda** | 서버리스 | `@hono/aws-lambda` |
900
+ | **Vercel** | Edge Functions | `vercel.json` |
901
+
902
+ ```typescript
903
+ // Node.js 어댑터
904
+ import { serve } from '@hono/node-server'
905
+ import app from './app'
250
906
 
251
- ## 관련 문서
907
+ serve({ fetch: app.fetch, port: 3000 })
908
+
909
+ // Cloudflare Workers
910
+ export default app
911
+
912
+ // Bun
913
+ export default app
914
+ ```
252
915
 
253
- - [Hono](../library/hono/index.md)
254
- - [Prisma](../library/prisma/index.md)
255
- - [Cloudflare 배포](../deployment/cloudflare.md)
916
+ </deployment>