@odvi/create-dtt-framework 0.1.3 → 0.1.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/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +16 -13
- package/dist/commands/create.js.map +1 -1
- package/package.json +3 -2
- package/template/.env.example +103 -0
- package/template/components.json +22 -0
- package/template/docs/framework/01-overview.md +289 -0
- package/template/docs/framework/02-techstack.md +503 -0
- package/template/docs/framework/api-layer.md +681 -0
- package/template/docs/framework/clerk-authentication.md +649 -0
- package/template/docs/framework/cli-installation.md +564 -0
- package/template/docs/framework/deployment/ci-cd.md +907 -0
- package/template/docs/framework/deployment/digitalocean.md +991 -0
- package/template/docs/framework/deployment/domain-setup.md +972 -0
- package/template/docs/framework/deployment/environment-variables.md +863 -0
- package/template/docs/framework/deployment/monitoring.md +927 -0
- package/template/docs/framework/deployment/production-checklist.md +649 -0
- package/template/docs/framework/deployment/vercel.md +791 -0
- package/template/docs/framework/environment-variables.md +658 -0
- package/template/docs/framework/health-check-system.md +582 -0
- package/template/docs/framework/implementation.md +559 -0
- package/template/docs/framework/snowflake-integration.md +591 -0
- package/template/docs/framework/state-management.md +615 -0
- package/template/docs/framework/supabase-integration.md +581 -0
- package/template/docs/framework/testing-guide.md +544 -0
- package/template/docs/framework/what-did-i-miss.md +526 -0
- package/template/drizzle.config.ts +12 -0
- package/template/next.config.js +21 -0
- package/template/postcss.config.js +5 -0
- package/template/prettier.config.js +4 -0
- package/template/public/favicon.ico +0 -0
- package/template/src/app/(auth)/layout.tsx +4 -0
- package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
- package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
- package/template/src/app/(dashboard)/health/page.tsx +16 -0
- package/template/src/app/(dashboard)/layout.tsx +17 -0
- package/template/src/app/api/[[...route]]/route.ts +11 -0
- package/template/src/app/api/debug-files/route.ts +33 -0
- package/template/src/app/api/webhooks/clerk/route.ts +112 -0
- package/template/src/app/layout.tsx +28 -0
- package/template/src/app/page.tsx +12 -0
- package/template/src/app/providers.tsx +20 -0
- package/template/src/components/layouts/navbar.tsx +14 -0
- package/template/src/components/shared/loading-spinner.tsx +6 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +62 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/collapsible.tsx +33 -0
- package/template/src/components/ui/scroll-area.tsx +58 -0
- package/template/src/components/ui/sheet.tsx +139 -0
- package/template/src/config/__tests__/env.test.ts +166 -0
- package/template/src/config/__tests__/site.test.ts +46 -0
- package/template/src/config/env.ts +36 -0
- package/template/src/config/site.ts +10 -0
- package/template/src/env.js +44 -0
- package/template/src/features/__tests__/health-check-config.test.ts +142 -0
- package/template/src/features/__tests__/health-check-types.test.ts +201 -0
- package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
- package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
- package/template/src/features/documentation/index.tsx +92 -0
- package/template/src/features/documentation/utils/doc-loader.ts +177 -0
- package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
- package/template/src/features/health-check/config.ts +72 -0
- package/template/src/features/health-check/index.ts +4 -0
- package/template/src/features/health-check/stores/health-store.ts +14 -0
- package/template/src/features/health-check/types.ts +18 -0
- package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
- package/template/src/hooks/queries/use-health-checks.ts +16 -0
- package/template/src/hooks/utils/use-debounce.ts +20 -0
- package/template/src/lib/__tests__/utils.test.ts +52 -0
- package/template/src/lib/__tests__/validators.test.ts +114 -0
- package/template/src/lib/nextbank/client.ts +37 -0
- package/template/src/lib/snowflake/client.ts +53 -0
- package/template/src/lib/supabase/admin.ts +7 -0
- package/template/src/lib/supabase/client.ts +7 -0
- package/template/src/lib/supabase/server.ts +23 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/lib/validators.ts +9 -0
- package/template/src/middleware.ts +22 -0
- package/template/src/server/api/index.ts +22 -0
- package/template/src/server/api/middleware/auth.ts +19 -0
- package/template/src/server/api/middleware/logger.ts +4 -0
- package/template/src/server/api/routes/health/clerk.ts +214 -0
- package/template/src/server/api/routes/health/database.ts +117 -0
- package/template/src/server/api/routes/health/edge-functions.ts +75 -0
- package/template/src/server/api/routes/health/framework.ts +45 -0
- package/template/src/server/api/routes/health/index.ts +102 -0
- package/template/src/server/api/routes/health/nextbank.ts +67 -0
- package/template/src/server/api/routes/health/snowflake.ts +83 -0
- package/template/src/server/api/routes/health/storage.ts +163 -0
- package/template/src/server/api/routes/users.ts +95 -0
- package/template/src/server/db/index.ts +17 -0
- package/template/src/server/db/queries/users.ts +8 -0
- package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
- package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
- package/template/src/server/db/schema/health-checks.ts +11 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/users.ts +16 -0
- package/template/src/server/db/schema.ts +26 -0
- package/template/src/stores/__tests__/ui-store.test.ts +87 -0
- package/template/src/stores/ui-store.ts +14 -0
- package/template/src/styles/globals.css +129 -0
- package/template/src/test/mocks/clerk.ts +35 -0
- package/template/src/test/mocks/snowflake.ts +28 -0
- package/template/src/test/mocks/supabase.ts +37 -0
- package/template/src/test/setup.ts +69 -0
- package/template/src/test/utils/test-helpers.ts +158 -0
- package/template/src/types/index.ts +14 -0
- package/template/tsconfig.json +43 -0
- package/template/vitest.config.ts +44 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
# DTT Framework - API Layer
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The DTT Framework uses [Hono](https://hono.dev/) as the API layer. Hono is a lightweight, fast web framework that provides an excellent developer experience with full TypeScript support.
|
|
6
|
+
|
|
7
|
+
### Why Hono?
|
|
8
|
+
|
|
9
|
+
- **Lightweight**: Small bundle size (~14KB)
|
|
10
|
+
- **Fast**: Built on Web Standards, optimized for performance
|
|
11
|
+
- **Type-Safe**: Full TypeScript support with inferred types
|
|
12
|
+
- **Middleware Support**: Composable middleware system
|
|
13
|
+
- **Flexible**: Works with multiple runtimes (Node.js, Edge, Deno)
|
|
14
|
+
- **Easy to Learn**: Simple, intuitive API
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Hono Framework Setup
|
|
19
|
+
|
|
20
|
+
### 1. Install Hono
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add hono
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Create Hono App
|
|
27
|
+
|
|
28
|
+
Create [`src/server/api/index.ts`](../../src/server/api/index.ts):
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Hono } from 'hono'
|
|
32
|
+
import { cors } from 'hono/cors'
|
|
33
|
+
import { logger } from 'hono/logger'
|
|
34
|
+
import { authMiddleware } from './middleware/auth'
|
|
35
|
+
import { healthRoutes } from './routes/health'
|
|
36
|
+
import { usersRoutes } from './routes/users'
|
|
37
|
+
|
|
38
|
+
const app = new Hono().basePath('/api')
|
|
39
|
+
|
|
40
|
+
// Global middleware
|
|
41
|
+
app.use('*', logger())
|
|
42
|
+
app.use('*', cors())
|
|
43
|
+
|
|
44
|
+
// Public routes
|
|
45
|
+
app.get('/ping', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }))
|
|
46
|
+
|
|
47
|
+
// Protected routes - require authentication
|
|
48
|
+
app.use('*', authMiddleware)
|
|
49
|
+
app.route('/health', healthRoutes)
|
|
50
|
+
app.route('/users', usersRoutes)
|
|
51
|
+
|
|
52
|
+
export { app }
|
|
53
|
+
export type AppType = typeof app
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Mount Hono in Next.js
|
|
57
|
+
|
|
58
|
+
Create [`src/app/api/[[...route]]/route.ts`](../../src/app/api/[[...route]]/route.ts):
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { handle } from 'hono/vercel'
|
|
62
|
+
import { app } from '@/server/api'
|
|
63
|
+
|
|
64
|
+
export const runtime = 'nodejs'
|
|
65
|
+
|
|
66
|
+
export const GET = handle(app)
|
|
67
|
+
export const POST = handle(app)
|
|
68
|
+
export const PUT = handle(app)
|
|
69
|
+
export const DELETE = handle(app)
|
|
70
|
+
export const PATCH = handle(app)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Route Organization
|
|
76
|
+
|
|
77
|
+
### Directory Structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/server/api/
|
|
81
|
+
├── index.ts # Hono app instance
|
|
82
|
+
├── middleware/ # Middleware functions
|
|
83
|
+
│ ├── auth.ts # Authentication middleware
|
|
84
|
+
│ └── logger.ts # Logger middleware
|
|
85
|
+
└── routes/ # Route definitions
|
|
86
|
+
├── health/ # Health check routes
|
|
87
|
+
│ ├── index.ts # Health route aggregation
|
|
88
|
+
│ ├── clerk.ts # Clerk health checks
|
|
89
|
+
│ ├── database.ts # Database health checks
|
|
90
|
+
│ ├── storage.ts # Storage health checks
|
|
91
|
+
│ ├── edge-functions.ts # Edge function checks
|
|
92
|
+
│ ├── snowflake.ts # Snowflake health checks
|
|
93
|
+
│ └── nextbank.ts # NextBank health checks
|
|
94
|
+
└── users.ts # User routes
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Route Aggregation
|
|
98
|
+
|
|
99
|
+
Health routes are aggregated in [`src/server/api/routes/health/index.ts`](../../src/server/api/routes/health/index.ts):
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { Hono } from 'hono'
|
|
103
|
+
import { clerkHealthRoutes } from './clerk'
|
|
104
|
+
import { databaseHealthRoutes } from './database'
|
|
105
|
+
import { storageHealthRoutes } from './storage'
|
|
106
|
+
import { edgeFunctionsHealthRoutes } from './edge-functions'
|
|
107
|
+
import { snowflakeHealthRoutes } from './snowflake'
|
|
108
|
+
import { nextbankHealthRoutes } from './nextbank'
|
|
109
|
+
|
|
110
|
+
export const healthRoutes = new Hono()
|
|
111
|
+
|
|
112
|
+
healthRoutes.route('/clerk', clerkHealthRoutes)
|
|
113
|
+
healthRoutes.route('/database', databaseHealthRoutes)
|
|
114
|
+
healthRoutes.route('/storage', storageHealthRoutes)
|
|
115
|
+
healthRoutes.route('/edge', edgeFunctionsHealthRoutes)
|
|
116
|
+
healthRoutes.route('/snowflake', snowflakeHealthRoutes)
|
|
117
|
+
healthRoutes.route('/nextbank', nextbankHealthRoutes)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Creating New Routes
|
|
121
|
+
|
|
122
|
+
**Step 1: Create route file**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// src/server/api/routes/products.ts
|
|
126
|
+
import { Hono } from 'hono'
|
|
127
|
+
import { db } from '@/server/db'
|
|
128
|
+
|
|
129
|
+
export const productsRoutes = new Hono()
|
|
130
|
+
|
|
131
|
+
productsRoutes.get('/', async (c) => {
|
|
132
|
+
const products = await db.query.products.findMany()
|
|
133
|
+
return c.json({ products })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
productsRoutes.get('/:id', async (c) => {
|
|
137
|
+
const id = c.req.param('id')
|
|
138
|
+
const product = await db.query.products.findFirst({
|
|
139
|
+
where: eq(products.id, id),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (!product) {
|
|
143
|
+
return c.json({ error: 'Product not found' }, 404)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return c.json({ product })
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Step 2: Register route in main app**
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// src/server/api/index.ts
|
|
154
|
+
import { productsRoutes } from './routes/products'
|
|
155
|
+
|
|
156
|
+
// Add to protected routes
|
|
157
|
+
app.route('/products', productsRoutes)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Middleware Usage
|
|
163
|
+
|
|
164
|
+
### Available Middleware
|
|
165
|
+
|
|
166
|
+
#### Logger Middleware
|
|
167
|
+
|
|
168
|
+
Located at [`src/server/api/middleware/logger.ts`](../../src/server/api/middleware/logger.ts):
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { logger } from 'hono/logger'
|
|
172
|
+
|
|
173
|
+
export { logger }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The logger middleware logs all incoming requests to the console:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
GET /api/ping 200 5ms
|
|
180
|
+
POST /api/health/database/write 200 123ms
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Auth Middleware
|
|
184
|
+
|
|
185
|
+
Located at [`src/server/api/middleware/auth.ts`](../../src/server/api/middleware/auth.ts):
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { createMiddleware } from 'hono/factory'
|
|
189
|
+
import { getAuth } from '@clerk/nextjs/server'
|
|
190
|
+
import type { NextRequest } from 'next/server'
|
|
191
|
+
|
|
192
|
+
export const authMiddleware = createMiddleware(async (c, next) => {
|
|
193
|
+
const request = c.req.raw as NextRequest
|
|
194
|
+
const auth = getAuth(request)
|
|
195
|
+
|
|
196
|
+
if (!auth.userId) {
|
|
197
|
+
return c.json({ error: 'Unauthorized' }, 401)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
c.set('auth', auth)
|
|
201
|
+
c.set('userId', auth.userId)
|
|
202
|
+
c.set('orgId', auth.orgId)
|
|
203
|
+
|
|
204
|
+
await next()
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Using Middleware
|
|
209
|
+
|
|
210
|
+
**Global middleware (applies to all routes):**
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { logger } from 'hono/logger'
|
|
214
|
+
import { cors } from 'hono/cors'
|
|
215
|
+
|
|
216
|
+
const app = new Hono()
|
|
217
|
+
|
|
218
|
+
app.use('*', logger())
|
|
219
|
+
app.use('*', cors())
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Route-specific middleware:**
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const protectedRoutes = new Hono()
|
|
226
|
+
|
|
227
|
+
protectedRoutes.use('*', authMiddleware)
|
|
228
|
+
protectedRoutes.get('/data', handler)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Middleware on specific routes:**
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
app.get('/public', handler)
|
|
235
|
+
app.get('/protected', authMiddleware, handler)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Creating Custom Middleware
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { createMiddleware } from 'hono/factory'
|
|
242
|
+
|
|
243
|
+
export const timingMiddleware = createMiddleware(async (c, next) => {
|
|
244
|
+
const start = Date.now()
|
|
245
|
+
await next()
|
|
246
|
+
const duration = Date.now() - start
|
|
247
|
+
c.header('X-Response-Time', `${duration}ms`)
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Middleware Order
|
|
252
|
+
|
|
253
|
+
Middleware is executed in the order they are defined:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
app.use('*', middleware1) // Executed first
|
|
257
|
+
app.use('*', middleware2) // Executed second
|
|
258
|
+
app.use('*', middleware3) // Executed third
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Authentication Middleware
|
|
264
|
+
|
|
265
|
+
### How It Works
|
|
266
|
+
|
|
267
|
+
The auth middleware:
|
|
268
|
+
|
|
269
|
+
1. Extracts the request from Hono context
|
|
270
|
+
2. Calls Clerk's `getAuth()` to verify session
|
|
271
|
+
3. Returns 401 if no user is authenticated
|
|
272
|
+
4. Sets auth data in context for downstream handlers
|
|
273
|
+
|
|
274
|
+
### Accessing Auth Data
|
|
275
|
+
|
|
276
|
+
**In route handlers:**
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
app.get('/protected', authMiddleware, async (c) => {
|
|
280
|
+
const userId = c.get('userId')
|
|
281
|
+
const orgId = c.get('orgId')
|
|
282
|
+
const auth = c.get('auth')
|
|
283
|
+
|
|
284
|
+
return c.json({ userId, orgId })
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Middleware Context Types
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import type { Context } from 'hono'
|
|
292
|
+
|
|
293
|
+
// Extend Hono context with your custom types
|
|
294
|
+
type Env = {
|
|
295
|
+
Variables: {
|
|
296
|
+
userId: string
|
|
297
|
+
orgId: string | null
|
|
298
|
+
auth: any
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const app = new Hono<Env>()
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Request/Response Patterns
|
|
308
|
+
|
|
309
|
+
### Request Handling
|
|
310
|
+
|
|
311
|
+
**Getting query parameters:**
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
app.get('/search', async (c) => {
|
|
315
|
+
const query = c.req.query('q')
|
|
316
|
+
const page = c.req.query('page') || '1'
|
|
317
|
+
const limit = c.req.query('limit') || '10'
|
|
318
|
+
|
|
319
|
+
return c.json({ query, page, limit })
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Getting path parameters:**
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
app.get('/users/:id', async (c) => {
|
|
327
|
+
const id = c.req.param('id')
|
|
328
|
+
return c.json({ userId: id })
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Getting request body:**
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
app.post('/users', async (c) => {
|
|
336
|
+
const body = await c.req.json()
|
|
337
|
+
const { name, email } = body
|
|
338
|
+
|
|
339
|
+
return c.json({ name, email })
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Getting headers:**
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
app.get('/data', async (c) => {
|
|
347
|
+
const authHeader = c.req.header('authorization')
|
|
348
|
+
const contentType = c.req.header('content-type')
|
|
349
|
+
|
|
350
|
+
return c.json({ authHeader, contentType })
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Response Handling
|
|
355
|
+
|
|
356
|
+
**JSON response:**
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
app.get('/data', (c) => {
|
|
360
|
+
return c.json({ message: 'Hello, World!' })
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Setting status code:**
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
app.get('/not-found', (c) => {
|
|
368
|
+
return c.json({ error: 'Not found' }, 404)
|
|
369
|
+
})
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Setting headers:**
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
app.get('/data', (c) => {
|
|
376
|
+
return c.json({ data: '...' }, 200, {
|
|
377
|
+
'X-Custom-Header': 'value',
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Text response:**
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
app.get('/text', (c) => {
|
|
386
|
+
return c.text('Plain text response')
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**File response:**
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
app.get('/download', (c) => {
|
|
394
|
+
return c.file('/path/to/file.pdf')
|
|
395
|
+
})
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Error Handling
|
|
399
|
+
|
|
400
|
+
**Try-catch pattern:**
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
app.get('/data', async (c) => {
|
|
404
|
+
try {
|
|
405
|
+
const data = await fetchData()
|
|
406
|
+
return c.json({ data })
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return c.json(
|
|
409
|
+
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
|
410
|
+
500
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Global error handler:**
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
app.onError((err, c) => {
|
|
420
|
+
console.error('Error:', err)
|
|
421
|
+
return c.json(
|
|
422
|
+
{ error: 'Internal server error' },
|
|
423
|
+
500
|
|
424
|
+
)
|
|
425
|
+
})
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Not found handler:**
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
app.notFound((c) => {
|
|
432
|
+
return c.json(
|
|
433
|
+
{ error: 'Not found' },
|
|
434
|
+
404
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Route Examples
|
|
442
|
+
|
|
443
|
+
### GET Request
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
app.get('/users', async (c) => {
|
|
447
|
+
const users = await db.select().from(usersTable)
|
|
448
|
+
return c.json({ users })
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### POST Request
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
app.post('/users', async (c) => {
|
|
456
|
+
const body = await c.req.json()
|
|
457
|
+
const { email, name } = body
|
|
458
|
+
|
|
459
|
+
const user = await db.insert(usersTable).values({
|
|
460
|
+
email,
|
|
461
|
+
name,
|
|
462
|
+
}).returning()
|
|
463
|
+
|
|
464
|
+
return c.json({ user: user[0] }, 201)
|
|
465
|
+
})
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### PUT Request
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
app.put('/users/:id', async (c) => {
|
|
472
|
+
const id = c.req.param('id')
|
|
473
|
+
const body = await c.req.json()
|
|
474
|
+
const { name } = body
|
|
475
|
+
|
|
476
|
+
const user = await db.update(usersTable)
|
|
477
|
+
.set({ name })
|
|
478
|
+
.where(eq(usersTable.id, id))
|
|
479
|
+
.returning()
|
|
480
|
+
|
|
481
|
+
return c.json({ user: user[0] })
|
|
482
|
+
})
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### DELETE Request
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
app.delete('/users/:id', async (c) => {
|
|
489
|
+
const id = c.req.param('id')
|
|
490
|
+
|
|
491
|
+
await db.delete(usersTable).where(eq(usersTable.id, id))
|
|
492
|
+
|
|
493
|
+
return c.json({ message: 'User deleted' })
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Type Safety
|
|
500
|
+
|
|
501
|
+
### Inferring Request Types
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import type { z } from 'zod'
|
|
505
|
+
|
|
506
|
+
const userSchema = z.object({
|
|
507
|
+
name: z.string(),
|
|
508
|
+
email: z.string().email(),
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
app.post('/users', async (c) => {
|
|
512
|
+
const body = await c.req.json()
|
|
513
|
+
const user = userSchema.parse(body)
|
|
514
|
+
|
|
515
|
+
// user is now typed as { name: string; email: string }
|
|
516
|
+
return c.json({ user })
|
|
517
|
+
})
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Inferring Response Types
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
const app = new Hono()
|
|
524
|
+
|
|
525
|
+
app.get('/data', (c) => {
|
|
526
|
+
return c.json<{ message: string }>({ message: 'Hello' })
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// Type-safe client
|
|
530
|
+
type AppType = typeof app
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Health Check Routes
|
|
536
|
+
|
|
537
|
+
### Available Health Check Endpoints
|
|
538
|
+
|
|
539
|
+
| Service | Endpoint | Method | Description |
|
|
540
|
+
|---------|----------|--------|-------------|
|
|
541
|
+
| **Clerk** | `/api/health/clerk/user` | GET | Get current user |
|
|
542
|
+
| **Clerk** | `/api/health/clerk/org` | GET | Get org membership |
|
|
543
|
+
| **Clerk** | `/api/health/clerk/members` | GET | List org members |
|
|
544
|
+
| **Database** | `/api/health/database/write` | POST | Write test row |
|
|
545
|
+
| **Database** | `/api/health/database/read` | GET | Read test row |
|
|
546
|
+
| **Database** | `/api/health/database/delete` | DELETE | Delete test row |
|
|
547
|
+
| **Storage** | `/api/health/storage/upload` | POST | Upload test file |
|
|
548
|
+
| **Storage** | `/api/health/storage/download` | GET | Download test file |
|
|
549
|
+
| **Storage** | `/api/health/storage/delete` | DELETE | Delete test file |
|
|
550
|
+
| **Edge Functions** | `/api/health/edge/ping` | GET | Ping edge function |
|
|
551
|
+
| **Edge Functions** | `/api/health/edge/auth` | GET | Test auth header |
|
|
552
|
+
| **Snowflake** | `/api/health/snowflake/connect` | GET | Test connection |
|
|
553
|
+
| **Snowflake** | `/api/health/snowflake/query` | GET | Execute test query |
|
|
554
|
+
| **NextBank** | `/api/health/nextbank/ping` | GET | Ping API |
|
|
555
|
+
| **NextBank** | `/api/health/nextbank/auth` | GET | Test authentication |
|
|
556
|
+
| **All** | `/api/health/all` | GET | Run all checks |
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Best Practices
|
|
561
|
+
|
|
562
|
+
### 1. Use TypeScript
|
|
563
|
+
|
|
564
|
+
Always leverage TypeScript for type safety:
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// ✅ Good - Type-safe
|
|
568
|
+
app.get('/users/:id', async (c) => {
|
|
569
|
+
const id = c.req.param('id')
|
|
570
|
+
// TypeScript knows id is a string
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// ❌ Bad - No type safety
|
|
574
|
+
app.get('/users/:id', async (c) => {
|
|
575
|
+
const id = c.req.param('id')
|
|
576
|
+
// Could be undefined
|
|
577
|
+
})
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 2. Validate Input
|
|
581
|
+
|
|
582
|
+
Always validate request input:
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import { z } from 'zod'
|
|
586
|
+
|
|
587
|
+
const createUserSchema = z.object({
|
|
588
|
+
email: z.string().email(),
|
|
589
|
+
name: z.string().min(2),
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
app.post('/users', async (c) => {
|
|
593
|
+
const body = await c.req.json()
|
|
594
|
+
const validated = createUserSchema.parse(body)
|
|
595
|
+
// validated is now type-safe
|
|
596
|
+
})
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### 3. Handle Errors Gracefully
|
|
600
|
+
|
|
601
|
+
Always handle errors properly:
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
app.get('/data', async (c) => {
|
|
605
|
+
try {
|
|
606
|
+
const data = await fetchData()
|
|
607
|
+
return c.json({ data })
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error('Error fetching data:', error)
|
|
610
|
+
return c.json(
|
|
611
|
+
{ error: 'Failed to fetch data' },
|
|
612
|
+
500
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### 4. Use Middleware for Cross-Cutting Concerns
|
|
619
|
+
|
|
620
|
+
Use middleware for authentication, logging, etc.:
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
app.use('*', authMiddleware)
|
|
624
|
+
app.use('*', logger())
|
|
625
|
+
app.use('*', timingMiddleware())
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 5. Keep Routes Focused
|
|
629
|
+
|
|
630
|
+
Keep each route focused on a single responsibility:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
// ✅ Good - Focused route
|
|
634
|
+
app.get('/users/:id', async (c) => {
|
|
635
|
+
const user = await getUserById(c.req.param('id'))
|
|
636
|
+
return c.json({ user })
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
// ❌ Bad - Doing too much
|
|
640
|
+
app.get('/users/:id', async (c) => {
|
|
641
|
+
const user = await getUserById(c.req.param('id'))
|
|
642
|
+
const posts = await getUserPosts(user.id)
|
|
643
|
+
const comments = await getUserComments(user.id)
|
|
644
|
+
const analytics = await getUserAnalytics(user.id)
|
|
645
|
+
return c.json({ user, posts, comments, analytics })
|
|
646
|
+
})
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Troubleshooting
|
|
652
|
+
|
|
653
|
+
### Common Issues
|
|
654
|
+
|
|
655
|
+
**Issue: Routes not found**
|
|
656
|
+
|
|
657
|
+
- Verify route is registered in main app
|
|
658
|
+
- Check base path configuration
|
|
659
|
+
- Ensure route file is imported
|
|
660
|
+
|
|
661
|
+
**Issue: Auth middleware not working**
|
|
662
|
+
|
|
663
|
+
- Verify Clerk is configured
|
|
664
|
+
- Check if auth token is being sent
|
|
665
|
+
- Ensure middleware is applied before route
|
|
666
|
+
|
|
667
|
+
**Issue: CORS errors**
|
|
668
|
+
|
|
669
|
+
- Verify CORS middleware is applied
|
|
670
|
+
- Check allowed origins configuration
|
|
671
|
+
- Ensure preflight requests are handled
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Related Documentation
|
|
676
|
+
|
|
677
|
+
- [Clerk Authentication](./clerk-authentication.md) - Authentication setup
|
|
678
|
+
- [Supabase Integration](./supabase-integration.md) - Database queries
|
|
679
|
+
- [Snowflake Integration](./snowflake-integration.md) - Snowflake queries
|
|
680
|
+
- [Health Check System](./health-check-system.md) - Health check endpoints
|
|
681
|
+
- [State Management](./state-management.md) - Data fetching patterns
|