@kood/claude-code 0.7.2 → 0.7.5
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.
- package/package.json +1 -1
- package/templates/.claude/instructions/agent-patterns/agent-teams-usage.md +152 -278
- package/templates/hono/docs/architecture.md +759 -98
- package/templates/tauri/CLAUDE.md +1 -0
- package/templates/tauri/docs/architecture.md +300 -0
- package/templates/tauri/docs/library/tauri/index.md +44 -356
- package/templates/tauri/docs/library/tauri/references/configuration.md +46 -0
- package/templates/tauri/docs/library/tauri/references/ipc-guide.md +131 -0
- package/templates/tauri/docs/library/tauri/references/plugins-guide.md +47 -0
- package/templates/tauri/docs/library/tauri/references/security-guide.md +64 -0
- package/templates/tauri/docs/library/tauri/references/state-guide.md +49 -0
- package/templates/tauri/docs/references/async-execution.md +66 -0
- package/templates/tauri/docs/references/data-flow.md +46 -0
- package/templates/tauri/docs/references/error-handling.md +113 -0
- package/templates/tauri/docs/references/mobile-architecture.md +64 -0
- package/templates/tauri/docs/references/security-model.md +85 -0
- package/templates/tauri/docs/references/state-management.md +117 -0
- package/templates/tauri/docs/references/tech-stack.md +31 -0
|
@@ -1,72 +1,200 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
|
20
|
-
├── routes/
|
|
21
|
-
├──
|
|
22
|
-
├──
|
|
23
|
-
|
|
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/
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
type Variables = { userId: string }
|
|
139
|
+
---
|
|
46
140
|
|
|
47
|
-
|
|
141
|
+
<folder_structure_advanced>
|
|
48
142
|
|
|
49
|
-
|
|
50
|
-
app.use('/api/*', cors())
|
|
143
|
+
## Clean Architecture (대규모)
|
|
51
144
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
209
|
+
// 목록 조회
|
|
210
|
+
users.get('/', async (c) => {
|
|
211
|
+
const result = await getUsers()
|
|
212
|
+
return c.json(result)
|
|
84
213
|
})
|
|
85
214
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
277
|
+
type AuthEnv = {
|
|
104
278
|
Bindings: { JWT_SECRET: string }
|
|
105
|
-
Variables: {
|
|
279
|
+
Variables: { user: { id: string; role: string } }
|
|
106
280
|
}
|
|
107
281
|
|
|
108
|
-
export const authMiddleware = createMiddleware<
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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(),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
165
|
-
|
|
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({
|
|
179
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
223
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
254
|
-
- [Prisma](../library/prisma/index.md)
|
|
255
|
-
- [Cloudflare 배포](../deployment/cloudflare.md)
|
|
916
|
+
</deployment>
|