@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.1
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/README.md +1046 -384
- package/dist/boss-D-fGtVgM.d.ts +187 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +167 -17
- package/dist/codegen/index.js +76 -1419
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1191 -0
- package/dist/config/index.js +264 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +728 -59
- package/dist/db/index.js +1028 -1225
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +579 -308
- package/dist/env/index.js +438 -930
- package/dist/env/index.js.map +1 -1
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +108 -0
- package/dist/event/index.js +122 -0
- package/dist/event/index.js.map +1 -0
- package/dist/job/index.d.ts +172 -0
- package/dist/job/index.js +361 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +2 -11
- package/dist/middleware/index.js +49 -703
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +416 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +288 -262
- package/dist/nextjs/server.js +568 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +667 -25
- package/dist/route/index.js +437 -1287
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +38 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/server/index.d.ts +201 -67
- package/dist/server/index.js +921 -3182
- package/dist/server/index.js.map +1 -1
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-DRG2XMTR.d.ts +157 -0
- package/package.json +52 -47
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
package/README.md
CHANGED
|
@@ -1,494 +1,1086 @@
|
|
|
1
|
-
# @spfn/core
|
|
1
|
+
# @spfn/core - Technical Architecture Documentation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Full-stack type-safe framework for building Next.js + Node.js applications with end-to-end type inference.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@spfn/core)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> **Alpha Release**: SPFN is currently in alpha. APIs may change. Use `@alpha` tag for installation.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Overview & Philosophy](#overview--philosophy)
|
|
16
|
+
- [System Architecture](#system-architecture)
|
|
17
|
+
- [Module Architecture](#module-architecture)
|
|
18
|
+
- [Type System](#type-system)
|
|
19
|
+
- [Integration Points](#integration-points)
|
|
20
|
+
- [Design Decisions](#design-decisions)
|
|
21
|
+
- [Extension Points](#extension-points)
|
|
22
|
+
- [Migration Guides](#migration-guides)
|
|
23
|
+
- [Module Exports](#module-exports)
|
|
24
|
+
- [Quick Reference](#quick-reference)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Overview & Philosophy
|
|
29
|
+
|
|
30
|
+
SPFN (Superfunction) is a full-stack TypeScript framework that provides **end-to-end type safety** from database to frontend with a **tRPC-inspired developer experience**.
|
|
31
|
+
|
|
32
|
+
### Core Principles
|
|
33
|
+
|
|
34
|
+
1. **Type Safety First**: Types flow from database schema → server routes → client API
|
|
35
|
+
2. **Developer Experience**: tRPC-style API with method chaining (`.params().query().call()`)
|
|
36
|
+
3. **Explicit over Magic**: No file-based routing, explicit imports for tree-shaking
|
|
37
|
+
4. **Security by Default**: HttpOnly cookies, API Route Proxy, environment isolation
|
|
38
|
+
5. **Production Ready**: Transaction management, read/write separation, graceful shutdown
|
|
39
|
+
|
|
40
|
+
### Design Philosophy
|
|
41
|
+
|
|
42
|
+
**Inspired by tRPC, Built for Production:**
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// tRPC-style API calls with structured input
|
|
46
|
+
const user = await api.getUser.call({
|
|
47
|
+
params: { id: '123' },
|
|
48
|
+
query: { include: 'posts' }
|
|
49
|
+
});
|
|
50
|
+
// ^? { id: string; name: string; email: string; posts: Post[] }
|
|
16
51
|
```
|
|
17
52
|
|
|
18
|
-
**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
53
|
+
**But with production features:**
|
|
54
|
+
- Cookie handling via API Route Proxy
|
|
55
|
+
- Transaction management with AsyncLocalStorage
|
|
56
|
+
- Read/Write database separation
|
|
57
|
+
- Graceful shutdown and health checks
|
|
58
|
+
- Lifecycle hooks for extensibility
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## System Architecture
|
|
63
|
+
|
|
64
|
+
### High-Level Overview
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
+---------------------------------------------------------------+
|
|
68
|
+
| Next.js Application |
|
|
69
|
+
| +----------------------------------------------------------+ |
|
|
70
|
+
| | Frontend (React) | |
|
|
71
|
+
| | - Server Components: SSR, ISR, Static | |
|
|
72
|
+
| | - Client Components: Interactive UI | |
|
|
73
|
+
| +---------------------------+------------------------------+ |
|
|
74
|
+
| | |
|
|
75
|
+
| | import { api } from '@spfn/...' |
|
|
76
|
+
| | api.getUser.call({ params }) |
|
|
77
|
+
| v |
|
|
78
|
+
| +----------------------------------------------------------+ |
|
|
79
|
+
| | RPC Proxy (Edge/Node.js) | |
|
|
80
|
+
| | app/api/rpc/[routeName]/route.ts | |
|
|
81
|
+
| | | |
|
|
82
|
+
| | 1. Resolve routeName → method/path from router | |
|
|
83
|
+
| | | |
|
|
84
|
+
| | 2. Request Interceptors | |
|
|
85
|
+
| | - Auth token injection | |
|
|
86
|
+
| | - Cookie forwarding | |
|
|
87
|
+
| | - Header manipulation | |
|
|
88
|
+
| | | |
|
|
89
|
+
| | 3. Forward to SPFN Server | |
|
|
90
|
+
| | fetch(SPFN_API_URL + resolvedPath) | |
|
|
91
|
+
| | | |
|
|
92
|
+
| | 4. Response Interceptors | |
|
|
93
|
+
| | - Set HttpOnly cookies | |
|
|
94
|
+
| | - Transform response | |
|
|
95
|
+
| | - Error handling | |
|
|
96
|
+
| +---------------------------+------------------------------+ |
|
|
97
|
+
+-------------------------------+--------------------------------+
|
|
98
|
+
|
|
|
99
|
+
| HTTP Request
|
|
100
|
+
v
|
|
101
|
+
+---------------------------------------------------------------+
|
|
102
|
+
| SPFN API Server (Node.js) |
|
|
103
|
+
| +----------------------------------------------------------+ |
|
|
104
|
+
| | Hono Web Framework | |
|
|
105
|
+
| | | |
|
|
106
|
+
| | 12-Step Middleware Pipeline: | |
|
|
107
|
+
| | 1. Logger | |
|
|
108
|
+
| | 2. CORS | |
|
|
109
|
+
| | 3. Global Middlewares | |
|
|
110
|
+
| | 4. Route-specific Middlewares | |
|
|
111
|
+
| | 5. Request Validation (TypeBox) | |
|
|
112
|
+
| | 6. Route Handler | |
|
|
113
|
+
| | 7-12. Response processing, error handling | |
|
|
114
|
+
| +---------------------------+------------------------------+ |
|
|
115
|
+
| | |
|
|
116
|
+
| | define-route System |
|
|
117
|
+
| v |
|
|
118
|
+
| +----------------------------------------------------------+ |
|
|
119
|
+
| | Route Handlers | |
|
|
120
|
+
| | - Type-safe input validation | |
|
|
121
|
+
| | - Transaction middleware | |
|
|
122
|
+
| | - Business logic | |
|
|
123
|
+
| +---------------------------+------------------------------+ |
|
|
124
|
+
| | |
|
|
125
|
+
| | Database Queries |
|
|
126
|
+
| v |
|
|
127
|
+
| +----------------------------------------------------------+ |
|
|
128
|
+
| | Database Layer (Drizzle ORM) | |
|
|
129
|
+
| | - Helper functions (findOne, create, etc.) | |
|
|
130
|
+
| | - Transaction propagation (AsyncLocalStorage) | |
|
|
131
|
+
| | - Read/Write separation | |
|
|
132
|
+
| +---------------------------+------------------------------+ |
|
|
133
|
+
+-------------------------------+--------------------------------+
|
|
134
|
+
|
|
|
135
|
+
| SQL Queries
|
|
136
|
+
v
|
|
137
|
+
+---------------------------------------------------------------+
|
|
138
|
+
| PostgreSQL Database |
|
|
139
|
+
| - Primary (Read/Write) |
|
|
140
|
+
| - Replica (Read-only) [optional] |
|
|
141
|
+
+---------------------------------------------------------------+
|
|
22
142
|
```
|
|
23
143
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
144
|
+
### Request Flow Example
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// 1. Client Call (Next.js Server Component)
|
|
148
|
+
// app/users/[id]/page.tsx
|
|
149
|
+
import { createApi } from '@spfn/core/nextjs';
|
|
150
|
+
import type { AppRouter } from '@/server/router';
|
|
151
|
+
|
|
152
|
+
const api = createApi<AppRouter>();
|
|
153
|
+
const user = await api.getUser.call({ params: { id: params.id } });
|
|
154
|
+
// → GET /api/rpc/getUser?input={"params":{"id":"123"}}
|
|
155
|
+
|
|
156
|
+
// 2. RPC Proxy
|
|
157
|
+
// app/api/rpc/[routeName]/route.ts
|
|
158
|
+
import { appRouter } from '@/server/router';
|
|
159
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
160
|
+
|
|
161
|
+
export const { GET, POST } = createRpcProxy({ router: appRouter });
|
|
162
|
+
// - Resolves routeName → method/path from router
|
|
163
|
+
// - Forwards to http://localhost:8790/users/123
|
|
164
|
+
// - Applies interceptors (auth, cookies)
|
|
165
|
+
|
|
166
|
+
// 3. SPFN Server Route Handler
|
|
167
|
+
// src/server/routes/users.ts
|
|
168
|
+
export const getUser = route.get('/users/:id')
|
|
169
|
+
.input({
|
|
170
|
+
params: Type.Object({ id: Type.String() }),
|
|
171
|
+
})
|
|
172
|
+
.handler(async (c) => {
|
|
173
|
+
const { params } = await c.data();
|
|
174
|
+
const user = await userRepo.findById(params.id);
|
|
175
|
+
return user;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 4. Database Query
|
|
179
|
+
// Drizzle ORM with helper function
|
|
180
|
+
// SELECT * FROM users WHERE id = $1
|
|
181
|
+
|
|
182
|
+
// 5. Response flows back through interceptors → proxy → client
|
|
27
183
|
```
|
|
28
184
|
|
|
29
|
-
|
|
185
|
+
---
|
|
30
186
|
|
|
31
|
-
|
|
187
|
+
## Module Architecture
|
|
32
188
|
|
|
33
|
-
|
|
34
|
-
// src/server/routes/users/contract.ts
|
|
35
|
-
import { Type } from '@sinclair/typebox';
|
|
189
|
+
### 1. Route System (`src/route/`)
|
|
36
190
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
total: Type.Number(),
|
|
51
|
-
}),
|
|
52
|
-
};
|
|
191
|
+
**Purpose**: Type-safe routing with automatic validation
|
|
192
|
+
|
|
193
|
+
**Architecture**:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
route.get('/users/:id')
|
|
197
|
+
.input({ params, query, body })
|
|
198
|
+
.handler(async (c) => { ... })
|
|
199
|
+
|
|
|
200
|
+
|-- Type Inference: RouteDef<TInput, TResponse>
|
|
201
|
+
|-- Validation: TypeBox schema
|
|
202
|
+
|-- Middleware: Skip control per route
|
|
203
|
+
|-- Response: c.success() / c.error()
|
|
53
204
|
```
|
|
54
205
|
|
|
55
|
-
|
|
206
|
+
**Key Components**:
|
|
56
207
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
import { findMany } from '@spfn/core/db';
|
|
62
|
-
import { users } from '../../entities/users.js';
|
|
208
|
+
- `defineRouter()`: Combines route definitions into typed router
|
|
209
|
+
- `route.get/post/put/patch/delete()`: Route builder with method chaining
|
|
210
|
+
- Input validation: Automatic TypeBox validation
|
|
211
|
+
- Middleware control: Per-route middleware skip
|
|
63
212
|
|
|
64
|
-
|
|
213
|
+
**Design Pattern**: Builder pattern with type inference
|
|
65
214
|
|
|
66
|
-
|
|
67
|
-
const { page = 1, limit = 10 } = c.query;
|
|
215
|
+
**[→ Full Documentation](./src/route/README.md)**
|
|
68
216
|
|
|
69
|
-
|
|
70
|
-
const offset = (page - 1) * limit;
|
|
71
|
-
const result = await findMany(users, { limit, offset });
|
|
217
|
+
---
|
|
72
218
|
|
|
73
|
-
|
|
74
|
-
});
|
|
219
|
+
### 2. Server System (`src/server/`)
|
|
75
220
|
|
|
76
|
-
|
|
221
|
+
**Purpose**: HTTP server with lifecycle management
|
|
222
|
+
|
|
223
|
+
**Architecture**:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Configuration Sources (Priority Order):
|
|
227
|
+
1. Runtime config (startServer({ port: 3000 }))
|
|
228
|
+
2. server.config.ts (defineServerConfig().build())
|
|
229
|
+
3. Environment variables (PORT, DATABASE_URL)
|
|
230
|
+
4. Defaults
|
|
231
|
+
|
|
232
|
+
|
|
|
233
|
+
v
|
|
234
|
+
Middleware Pipeline:
|
|
235
|
+
1. Request Logger
|
|
236
|
+
2. CORS
|
|
237
|
+
3. Global Middlewares
|
|
238
|
+
4. Named Middlewares
|
|
239
|
+
5. beforeRoutes hook
|
|
240
|
+
6. Route Registration
|
|
241
|
+
7. afterRoutes hook
|
|
242
|
+
8. Route-specific Middlewares
|
|
243
|
+
9. Request Validation
|
|
244
|
+
10. Route Handler Execution
|
|
245
|
+
11. Response Serialization
|
|
246
|
+
12. Error Handler
|
|
247
|
+
|
|
248
|
+
|
|
|
249
|
+
v
|
|
250
|
+
Lifecycle Hooks:
|
|
251
|
+
- beforeInfrastructure
|
|
252
|
+
- afterInfrastructure
|
|
253
|
+
- beforeRoutes
|
|
254
|
+
- afterRoutes
|
|
255
|
+
- afterStart
|
|
256
|
+
- beforeShutdown
|
|
77
257
|
```
|
|
78
258
|
|
|
79
|
-
|
|
259
|
+
**Key Components**:
|
|
80
260
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
│ - Handle requests/responses │
|
|
95
|
-
│ - Thin handlers │
|
|
96
|
-
└──────────────┬──────────────────────────┘
|
|
97
|
-
│
|
|
98
|
-
┌──────────────▼──────────────────────────┐
|
|
99
|
-
│ Service Layer │ Business logic
|
|
100
|
-
│ - Orchestrate operations │
|
|
101
|
-
│ - Implement business rules │
|
|
102
|
-
│ - Use helper functions or custom logic │
|
|
103
|
-
└──────────────┬──────────────────────────┘
|
|
104
|
-
│
|
|
105
|
-
┌──────────────▼──────────────────────────┐
|
|
106
|
-
│ Data Access Layer │ Database operations
|
|
107
|
-
│ - Use helper functions (findOne, etc) │
|
|
108
|
-
│ - Custom queries with Drizzle │
|
|
109
|
-
│ - Domain-specific wrappers │
|
|
110
|
-
└──────────────┬──────────────────────────┘
|
|
111
|
-
│
|
|
112
|
-
┌──────────────▼──────────────────────────┐
|
|
113
|
-
│ Entity Layer │ Database schema
|
|
114
|
-
│ - Table definitions (Drizzle) │
|
|
115
|
-
│ - Type inference │
|
|
116
|
-
│ - Schema helpers │
|
|
117
|
-
└─────────────────────────────────────────┘
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Complete Example: Blog Post System
|
|
121
|
-
|
|
122
|
-
**1. Entity Layer** - Define database schema
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// src/server/entities/posts.ts
|
|
126
|
-
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
127
|
-
import { id, timestamps } from '@spfn/core/db';
|
|
128
|
-
|
|
129
|
-
export const posts = pgTable('posts', {
|
|
130
|
-
id: id(),
|
|
131
|
-
title: text('title').notNull(),
|
|
132
|
-
slug: text('slug').notNull().unique(),
|
|
133
|
-
content: text('content').notNull(),
|
|
134
|
-
status: text('status', {
|
|
135
|
-
enum: ['draft', 'published']
|
|
136
|
-
}).notNull().default('draft'),
|
|
137
|
-
...timestamps(),
|
|
138
|
-
});
|
|
261
|
+
- `defineServerConfig()`: Fluent configuration builder
|
|
262
|
+
- `createServer()`: Creates Hono app with routes
|
|
263
|
+
- `startServer()`: Starts HTTP server with lifecycle
|
|
264
|
+
- Lifecycle hooks: beforeInfrastructure, afterInfrastructure, beforeRoutes, afterRoutes, afterStart, beforeShutdown
|
|
265
|
+
- Graceful shutdown: SIGTERM/SIGINT handling
|
|
266
|
+
|
|
267
|
+
**Design Pattern**: Builder + Lifecycle hooks
|
|
268
|
+
|
|
269
|
+
**[→ Full Documentation](./src/server/README.md)**
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### 3. Database System (`src/db/`)
|
|
139
274
|
|
|
140
|
-
|
|
141
|
-
|
|
275
|
+
**Purpose**: Type-safe database operations with transactions
|
|
276
|
+
|
|
277
|
+
**Architecture**:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
Application Code
|
|
281
|
+
|
|
|
282
|
+
| findOne(users, { id: 1 })
|
|
283
|
+
v
|
|
284
|
+
Helper Functions (Facade)
|
|
285
|
+
|
|
|
286
|
+
| Check AsyncLocalStorage for transaction
|
|
287
|
+
v
|
|
288
|
+
Transaction Context?
|
|
289
|
+
|-- Yes: Use transaction instance
|
|
290
|
+
|-- No: Use default database connection
|
|
291
|
+
|
|
|
292
|
+
v
|
|
293
|
+
Drizzle ORM Query Builder
|
|
294
|
+
|
|
|
295
|
+
v
|
|
296
|
+
PostgreSQL (Primary or Replica)
|
|
142
297
|
```
|
|
143
298
|
|
|
144
|
-
**
|
|
299
|
+
**Key Components**:
|
|
300
|
+
|
|
301
|
+
- Helper functions: `findOne`, `findMany`, `create`, `update`, `delete`, `count`
|
|
302
|
+
- Transaction middleware: `Transactional()` with AsyncLocalStorage propagation
|
|
303
|
+
- Read/Write separation: Automatic routing based on operation
|
|
304
|
+
- Schema helpers: `id()`, `timestamps()`, `foreignKey()`, `enumText()`
|
|
305
|
+
|
|
306
|
+
**Design Pattern**: Facade + AsyncLocalStorage for transaction propagation
|
|
307
|
+
|
|
308
|
+
**[→ Full Documentation](./src/db/README.md)**
|
|
309
|
+
|
|
310
|
+
**Transaction Flow**:
|
|
145
311
|
|
|
146
312
|
```typescript
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
313
|
+
// Route with Transactional middleware
|
|
314
|
+
app.bind(createUserContract, [Transactional()], async (c) => {
|
|
315
|
+
// All database operations use the same transaction
|
|
316
|
+
const user = await create(users, { name: 'John' });
|
|
317
|
+
const profile = await create(profiles, { userId: user.id });
|
|
318
|
+
|
|
319
|
+
// Auto-commit on success
|
|
320
|
+
// Auto-rollback on error
|
|
321
|
+
return c.json(user);
|
|
322
|
+
});
|
|
323
|
+
```
|
|
151
324
|
|
|
152
|
-
|
|
153
|
-
export async function findPostBySlug(slug: string): Promise<Post | null> {
|
|
154
|
-
return findOne(posts, { slug });
|
|
155
|
-
}
|
|
325
|
+
---
|
|
156
326
|
|
|
157
|
-
|
|
158
|
-
return findMany(posts, {
|
|
159
|
-
where: { status: 'published' },
|
|
160
|
-
orderBy: desc(posts.createdAt)
|
|
161
|
-
});
|
|
162
|
-
}
|
|
327
|
+
### 4. Client System (`src/nextjs/`)
|
|
163
328
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
329
|
+
**Purpose**: Type-safe API client for Next.js with tRPC-style DX
|
|
330
|
+
|
|
331
|
+
**Architecture**:
|
|
167
332
|
|
|
168
|
-
// Or use helper functions directly in routes for simple cases
|
|
169
|
-
// const post = await findOne(posts, { id: 1 });
|
|
170
333
|
```
|
|
334
|
+
Client Code
|
|
335
|
+
|
|
|
336
|
+
| api.getUser.call({ params: { id: '123' } })
|
|
337
|
+
v
|
|
338
|
+
ApiClient (Proxy)
|
|
339
|
+
|
|
|
340
|
+
| body 유무로 GET/POST 결정
|
|
341
|
+
| GET /api/rpc/getUser?input={...}
|
|
342
|
+
| POST /api/rpc/createUser body: {...}
|
|
343
|
+
v
|
|
344
|
+
fetch() to Next.js API Route
|
|
345
|
+
|
|
|
346
|
+
v
|
|
347
|
+
RpcProxy (API Route Handler)
|
|
348
|
+
|
|
|
349
|
+
| 1. Extract routeName from URL
|
|
350
|
+
| 2. Lookup method/path from appRouter
|
|
351
|
+
| 3. Execute request interceptors
|
|
352
|
+
| 4. Forward to SPFN_API_URL with resolved path
|
|
353
|
+
| 5. Execute response interceptors
|
|
354
|
+
| 6. Set cookies from ctx.setCookies
|
|
355
|
+
v
|
|
356
|
+
Return NextResponse
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Key Components**:
|
|
360
|
+
|
|
361
|
+
- `createApi()`: tRPC-style client with type inference (no metadata needed)
|
|
362
|
+
- `createRpcProxy()`: RPC-style API Route handler with route resolution
|
|
363
|
+
- `registerInterceptors()`: Registry for plugin interceptors
|
|
364
|
+
- Path matching: Wildcard and param support (`/_auth/*`, `/users/:id`)
|
|
365
|
+
- Cookie handling: `setCookies` array in response interceptors
|
|
366
|
+
|
|
367
|
+
**Design Pattern**: Proxy for client, RPC resolution for proxy
|
|
368
|
+
|
|
369
|
+
**[→ Full Documentation](./src/nextjs/README.md)**
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
### 5. Error System (`src/errors/`)
|
|
374
|
+
|
|
375
|
+
**Purpose**: Structured error handling with HTTP status codes
|
|
376
|
+
|
|
377
|
+
**Key Components**:
|
|
378
|
+
|
|
379
|
+
- `ApiError`: Base error class
|
|
380
|
+
- `ValidationError`: Input validation failures (400)
|
|
381
|
+
- `NotFoundError`: Resource not found (404)
|
|
382
|
+
- `ConflictError`: Duplicate resources (409)
|
|
383
|
+
- `UnauthorizedError`: Authentication required (401)
|
|
384
|
+
|
|
385
|
+
**[→ Full Documentation](./src/errors/README.md)**
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### 6. Logger System (`src/logger/`)
|
|
390
|
+
|
|
391
|
+
**Purpose**: Structured logging with transports and masking
|
|
392
|
+
|
|
393
|
+
**Key Components**:
|
|
394
|
+
|
|
395
|
+
- Adapter pattern: Pino (default) or custom
|
|
396
|
+
- Sensitive data masking: Passwords, tokens, API keys
|
|
397
|
+
- File rotation: Date and size-based with cleanup
|
|
398
|
+
- Multiple transports: Console, File, Slack, Email
|
|
399
|
+
|
|
400
|
+
**[→ Full Documentation](./src/logger/README.md)**
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
### 7. Cache System (`src/cache/`)
|
|
405
|
+
|
|
406
|
+
**Purpose**: Valkey/Redis integration with master-replica support
|
|
407
|
+
|
|
408
|
+
**Key Components**:
|
|
409
|
+
|
|
410
|
+
- `initCache()`: Initialize connections
|
|
411
|
+
- `getCache()`: Write operations (master)
|
|
412
|
+
- `getCacheRead()`: Read operations (replica or master)
|
|
413
|
+
- `isCacheDisabled()`: Check if cache is disabled
|
|
414
|
+
- Graceful degradation when cache unavailable
|
|
415
|
+
|
|
416
|
+
**[→ Full Documentation](./src/cache/README.md)**
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
### 8. Middleware System (`src/middleware/`)
|
|
421
|
+
|
|
422
|
+
**Purpose**: Named middleware with skip control
|
|
423
|
+
|
|
424
|
+
**Key Components**:
|
|
425
|
+
|
|
426
|
+
- `defineMiddleware()`: Create named middleware
|
|
427
|
+
- Skip control: Routes can skip specific middlewares
|
|
428
|
+
- Built-in: Logger, CORS, Error handler
|
|
171
429
|
|
|
172
|
-
**
|
|
430
|
+
**[→ Full Documentation](./src/middleware/README.md)**
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Type System
|
|
435
|
+
|
|
436
|
+
### End-to-End Type Safety Flow
|
|
173
437
|
|
|
174
438
|
```typescript
|
|
175
|
-
//
|
|
439
|
+
// 1. Database Schema (Source of Truth)
|
|
440
|
+
// src/server/entities/users.ts
|
|
441
|
+
export const users = pgTable('users', {
|
|
442
|
+
id: id(),
|
|
443
|
+
name: text('name').notNull(),
|
|
444
|
+
email: text('email').notNull().unique(),
|
|
445
|
+
...timestamps(),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
export type User = typeof users.$inferSelect;
|
|
449
|
+
// ^? { id: string; name: string; email: string; createdAt: Date; updatedAt: Date }
|
|
450
|
+
|
|
451
|
+
// 2. Server Route Definition
|
|
452
|
+
// src/server/routes/users.ts
|
|
453
|
+
import { route } from '@spfn/core/route';
|
|
176
454
|
import { Type } from '@sinclair/typebox';
|
|
177
455
|
|
|
178
|
-
export const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
456
|
+
export const getUser = route.get('/users/:id')
|
|
457
|
+
.input({
|
|
458
|
+
params: Type.Object({
|
|
459
|
+
id: Type.String(),
|
|
460
|
+
}),
|
|
461
|
+
query: Type.Object({
|
|
462
|
+
include: Type.Optional(Type.String()),
|
|
463
|
+
}),
|
|
464
|
+
})
|
|
465
|
+
.handler(async (c) => {
|
|
466
|
+
const { params, query } = await c.data();
|
|
467
|
+
// ^? { params: { id: string }, query: { include?: string } }
|
|
468
|
+
|
|
469
|
+
const user = await findOne(users, { id: params.id });
|
|
470
|
+
// ^? User | null
|
|
471
|
+
|
|
472
|
+
return c.success(user);
|
|
473
|
+
// ^? Response inferred from return value
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// 3. Router Type Export
|
|
477
|
+
// src/server/router.ts
|
|
478
|
+
export const appRouter = defineRouter({
|
|
479
|
+
getUser,
|
|
480
|
+
// ... other routes
|
|
481
|
+
});
|
|
191
482
|
|
|
192
|
-
export
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
483
|
+
export type AppRouter = typeof appRouter;
|
|
484
|
+
// ^? Router<{ getUser: RouteDef<..., ...>, ... }>
|
|
485
|
+
|
|
486
|
+
// 4. Client API Call (Next.js)
|
|
487
|
+
// app/users/[id]/page.tsx
|
|
488
|
+
import { api } from '@spfn/core/nextjs/server';
|
|
489
|
+
|
|
490
|
+
const user = await api.getUser
|
|
491
|
+
.params({ id: '123' }) // Type-checked: must be { id: string }
|
|
492
|
+
.query({ include: 'posts' }) // Type-checked: { include?: string }
|
|
493
|
+
.call();
|
|
494
|
+
// ^? User (inferred from server handler return type)
|
|
495
|
+
|
|
496
|
+
// Full type inference chain:
|
|
497
|
+
// User type → RouteDef → Router → Client API → return type
|
|
201
498
|
```
|
|
202
499
|
|
|
500
|
+
### Type Inference Utilities
|
|
501
|
+
|
|
203
502
|
```typescript
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
503
|
+
// Extract input type from route
|
|
504
|
+
type GetUserInput = InferRouteInput<typeof getUser>;
|
|
505
|
+
// ^? { params: { id: string }; query: { include?: string } }
|
|
506
|
+
|
|
507
|
+
// Extract output type from route
|
|
508
|
+
type GetUserOutput = InferRouteOutput<typeof getUser>;
|
|
509
|
+
// ^? User
|
|
510
|
+
|
|
511
|
+
// Client type inference
|
|
512
|
+
type ApiClient<TRouter> = {
|
|
513
|
+
[K in keyof TRouter['routes']]: TRouter['routes'][K] extends RouteDef<infer TInput, infer TOutput>
|
|
514
|
+
? RouteClient<TInput, TOutput>
|
|
515
|
+
: never;
|
|
516
|
+
};
|
|
517
|
+
```
|
|
210
518
|
|
|
211
|
-
|
|
519
|
+
---
|
|
212
520
|
|
|
213
|
-
|
|
521
|
+
## Integration Points
|
|
522
|
+
|
|
523
|
+
### 1. Server ↔ Database: Transaction Context
|
|
524
|
+
|
|
525
|
+
**Mechanism**: AsyncLocalStorage propagates transaction across async calls
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// Route handler
|
|
214
529
|
app.bind(createPostContract, [Transactional()], async (c) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Generate slug from title
|
|
218
|
-
const slug = body.title.toLowerCase()
|
|
219
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
220
|
-
.replace(/(^-|-$)/g, '');
|
|
221
|
-
|
|
222
|
-
// Check if slug exists
|
|
223
|
-
const existing = await findPostBySlug(slug);
|
|
224
|
-
if (existing) {
|
|
225
|
-
throw new ConflictError('Post with this title already exists', { slug });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Create post
|
|
229
|
-
const post = await createPost({
|
|
230
|
-
...body,
|
|
231
|
-
slug,
|
|
232
|
-
status: 'draft'
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// ✅ Auto-commit on success, auto-rollback on error
|
|
236
|
-
return c.json(post, 201);
|
|
237
|
-
});
|
|
530
|
+
// AsyncLocalStorage stores transaction
|
|
531
|
+
const post = await create(posts, { title: 'Hello' });
|
|
238
532
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
533
|
+
// Same transaction is used automatically
|
|
534
|
+
const tag = await create(tags, { postId: post.id, name: 'news' });
|
|
535
|
+
|
|
536
|
+
// Auto-commit on success, auto-rollback on error
|
|
537
|
+
return c.json(post);
|
|
243
538
|
});
|
|
539
|
+
```
|
|
244
540
|
|
|
245
|
-
|
|
541
|
+
**Why**: No need to pass transaction object through function calls
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
### 2. Server ↔ Client: Type Inference
|
|
546
|
+
|
|
547
|
+
**Mechanism**: `typeof appRouter` captures all route types
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
// Server: Export router type
|
|
551
|
+
export const appRouter = defineRouter({ getUser, createUser });
|
|
552
|
+
export type AppRouter = typeof appRouter;
|
|
553
|
+
|
|
554
|
+
// Client: Import type (not value!)
|
|
555
|
+
import type { AppRouter } from '@/server/router';
|
|
556
|
+
const api = createApi<AppRouter>(/* ... */);
|
|
557
|
+
|
|
558
|
+
// Automatic type inference for all routes
|
|
559
|
+
const user = await api.getUser.params({ id: '123' }).call();
|
|
560
|
+
// ^? Full type safety without manual type definitions
|
|
246
561
|
```
|
|
247
562
|
|
|
248
|
-
|
|
563
|
+
**Why**: Single source of truth (server) → client types automatically sync
|
|
249
564
|
|
|
250
|
-
|
|
251
|
-
- Each layer has a single responsibility
|
|
252
|
-
- Easy to locate and modify code
|
|
565
|
+
---
|
|
253
566
|
|
|
254
|
-
|
|
255
|
-
- Test each layer independently
|
|
256
|
-
- Mock dependencies easily
|
|
567
|
+
### 3. Next.js ↔ SPFN: RPC Proxy
|
|
257
568
|
|
|
258
|
-
|
|
259
|
-
- Services can be used by multiple routes
|
|
260
|
-
- Data access functions can be shared across services
|
|
569
|
+
**Mechanism**: Next.js API Route resolves routeName and forwards to SPFN server
|
|
261
570
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
571
|
+
```typescript
|
|
572
|
+
// app/api/rpc/[routeName]/route.ts
|
|
573
|
+
import { appRouter } from '@/server/router';
|
|
574
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
265
575
|
|
|
266
|
-
|
|
267
|
-
- Add features without breaking existing code
|
|
268
|
-
- Clear boundaries prevent coupling
|
|
576
|
+
export const { GET, POST } = createRpcProxy({ router: appRouter });
|
|
269
577
|
|
|
270
|
-
|
|
578
|
+
// Automatically:
|
|
579
|
+
// 1. Resolves routeName → method/path from router
|
|
580
|
+
// 2. Forwards to http://localhost:8790/{resolved-path}
|
|
581
|
+
// 3. Applies interceptors (auth, cookies)
|
|
582
|
+
// 4. Returns NextResponse
|
|
583
|
+
```
|
|
271
584
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
| **Routes** | HTTP interface | Contracts, request handling |
|
|
585
|
+
**Why**:
|
|
586
|
+
- HttpOnly cookie support (browser → Next.js includes cookies automatically)
|
|
587
|
+
- Security (SPFN_API_URL hidden from browser)
|
|
588
|
+
- No CORS (same-origin requests)
|
|
589
|
+
- No metadata codegen required
|
|
278
590
|
|
|
279
|
-
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
### 4. Packages: Registry System
|
|
594
|
+
|
|
595
|
+
**Mechanism**: Packages register interceptors on import
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// @spfn/auth package
|
|
599
|
+
import { registerInterceptors } from '@spfn/core/nextjs/server';
|
|
600
|
+
|
|
601
|
+
registerInterceptors('auth', [
|
|
602
|
+
{
|
|
603
|
+
pathPattern: '/_auth/*',
|
|
604
|
+
response: async (ctx, next) => {
|
|
605
|
+
// Set session cookie
|
|
606
|
+
ctx.setCookies.push({ name: 'session', value: token });
|
|
607
|
+
await next();
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
]);
|
|
611
|
+
|
|
612
|
+
// App: Auto-discovery
|
|
613
|
+
import '@spfn/auth/adapters/nextjs'; // Registers interceptors
|
|
614
|
+
export { GET, POST } from '@spfn/core/nextjs/server'; // Uses registered interceptors
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Why**: Zero-config integration for plugin packages
|
|
618
|
+
|
|
619
|
+
---
|
|
280
620
|
|
|
281
|
-
|
|
282
|
-
- ✅ Use schema helpers: `id()`, `timestamps()`
|
|
283
|
-
- ✅ Export inferred types: `Post`, `NewPost`
|
|
284
|
-
- ✅ Use TEXT with enum for status fields
|
|
621
|
+
## Design Decisions
|
|
285
622
|
|
|
286
|
-
|
|
287
|
-
- ✅ Use helper functions for simple CRUD: `findOne()`, `create()`, etc.
|
|
288
|
-
- ✅ Create domain-specific wrappers in `src/server/repositories/*.repository.ts`
|
|
289
|
-
- ✅ Export functions (not classes): `export async function findPostBySlug()`
|
|
290
|
-
- ✅ Use object-based where for simple queries: `{ id: 1 }`
|
|
291
|
-
- ✅ Use SQL-based where for complex queries: `and(eq(...), gt(...))`
|
|
292
|
-
- ✅ Full TypeScript type inference from table schemas
|
|
623
|
+
### 1. Why tRPC-Style API over REST Client?
|
|
293
624
|
|
|
294
|
-
**
|
|
295
|
-
- ✅ Keep handlers thin (delegate to services/data access)
|
|
296
|
-
- ✅ Define contracts with TypeBox
|
|
297
|
-
- ✅ Use `Transactional()` middleware for write operations
|
|
298
|
-
- ✅ Use `c.data()` for validated input
|
|
299
|
-
- ✅ Return `c.json()` responses
|
|
625
|
+
**Decision**: Method chaining with `.params().query().call()`
|
|
300
626
|
|
|
301
|
-
|
|
627
|
+
**Reasons**:
|
|
628
|
+
- Better DX: Fluent API guides usage
|
|
629
|
+
- Type safety: Each method is type-checked
|
|
630
|
+
- Flexibility: Optional parameters can be omitted
|
|
631
|
+
- Discovery: IDE autocomplete shows available options
|
|
302
632
|
|
|
303
|
-
|
|
304
|
-
|
|
633
|
+
**Example**:
|
|
634
|
+
```typescript
|
|
635
|
+
// tRPC-style (SPFN)
|
|
636
|
+
const user = await api.getUser
|
|
637
|
+
.params({ id: '123' })
|
|
638
|
+
.query({ include: 'posts' })
|
|
639
|
+
.call();
|
|
640
|
+
|
|
641
|
+
// vs Traditional REST client
|
|
642
|
+
const user = await client.get('/users/123', {
|
|
643
|
+
params: { include: 'posts' } // No type safety
|
|
644
|
+
});
|
|
645
|
+
```
|
|
305
646
|
|
|
306
|
-
|
|
647
|
+
---
|
|
307
648
|
|
|
308
|
-
|
|
309
|
-
- Automatic route discovery (`index.ts`, `[id].ts`, `[...slug].ts`)
|
|
310
|
-
- Contract-based validation with TypeBox
|
|
311
|
-
- Type-safe request/response handling
|
|
312
|
-
- Method-level middleware control (skip auth per HTTP method)
|
|
649
|
+
### 2. Why API Route Proxy Pattern?
|
|
313
650
|
|
|
314
|
-
|
|
315
|
-
Drizzle ORM integration with type-safe helper functions and automatic transaction handling.
|
|
651
|
+
**Decision**: Next.js API Route forwards to SPFN server
|
|
316
652
|
|
|
317
|
-
**
|
|
653
|
+
**Reasons**:
|
|
654
|
+
- **HttpOnly Cookies**: Browser automatically sends cookies to same-origin
|
|
655
|
+
- **Security**: SPFN API URL hidden from browser
|
|
656
|
+
- **No CORS**: Same-origin requests (localhost:3000 → localhost:3000)
|
|
657
|
+
- **Unified Path**: Server Components and Client Components use same API
|
|
318
658
|
|
|
319
|
-
**
|
|
320
|
-
-
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
- Hybrid where clause support (objects or SQL)
|
|
324
|
-
- **Function schema auto-discovery** (see below)
|
|
659
|
+
**Alternative Rejected**: Direct calls from browser to SPFN server
|
|
660
|
+
- Would expose SPFN_API_URL to browser
|
|
661
|
+
- CORS configuration required
|
|
662
|
+
- Cannot use HttpOnly cookies from browser
|
|
325
663
|
|
|
326
|
-
|
|
327
|
-
Automatic discovery of database schemas from Superfunction ecosystem functions.
|
|
664
|
+
---
|
|
328
665
|
|
|
329
|
-
|
|
666
|
+
### 3. Why define-route over File-Based Routing?
|
|
330
667
|
|
|
331
|
-
**
|
|
332
|
-
- Zero-config schema discovery from `@spfn/*` functions
|
|
333
|
-
- Functions declare schemas via `package.json`
|
|
334
|
-
- No hard dependencies between functions
|
|
335
|
-
- Efficient scanning (direct dependencies only)
|
|
336
|
-
- Function-specific migration support
|
|
668
|
+
**Decision**: Explicit route imports with `defineRouter()`
|
|
337
669
|
|
|
338
|
-
**
|
|
670
|
+
**Reasons**:
|
|
671
|
+
- **Explicit Imports**: Better tree-shaking, clear dependencies
|
|
672
|
+
- **Type Safety**: `typeof appRouter` captures all routes
|
|
673
|
+
- **Flexibility**: Routes can be defined anywhere
|
|
674
|
+
- **No Magic**: No file system scanning at runtime
|
|
675
|
+
|
|
676
|
+
**Alternative Rejected**: File-based routing (like Next.js)
|
|
677
|
+
- Runtime file system scanning
|
|
678
|
+
- Implicit route registration
|
|
679
|
+
- Harder to trace route definitions
|
|
680
|
+
- Less flexible for monorepo setups
|
|
681
|
+
|
|
682
|
+
**Migration Path**: Deprecated contract-based and file-based routing systems
|
|
683
|
+
|
|
684
|
+
---
|
|
339
685
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
686
|
+
### 4. Why AsyncLocalStorage for Transactions?
|
|
687
|
+
|
|
688
|
+
**Decision**: Transaction propagation without explicit passing
|
|
689
|
+
|
|
690
|
+
**Reasons**:
|
|
691
|
+
- **Clean API**: No need to pass `tx` object through all functions
|
|
692
|
+
- **Automatic**: Works across async boundaries
|
|
693
|
+
- **Safe**: Isolated per request
|
|
694
|
+
- **Compatible**: Works with existing code
|
|
695
|
+
|
|
696
|
+
**Example**:
|
|
697
|
+
```typescript
|
|
698
|
+
// With AsyncLocalStorage (SPFN)
|
|
699
|
+
async function createPostWithTags(data) {
|
|
700
|
+
const post = await create(posts, data);
|
|
701
|
+
const tags = await createMany(tags, data.tags.map(t => ({ postId: post.id, ...t })));
|
|
702
|
+
// Same transaction automatically
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// vs Explicit transaction passing
|
|
706
|
+
async function createPostWithTags(tx, data) {
|
|
707
|
+
const post = await tx.insert(posts).values(data);
|
|
708
|
+
const tags = await tx.insert(tags).values(...);
|
|
709
|
+
// Must pass tx everywhere
|
|
348
710
|
}
|
|
349
711
|
```
|
|
350
712
|
|
|
351
|
-
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
### 5. Why Hono over Express?
|
|
716
|
+
|
|
717
|
+
**Decision**: Hono as the underlying web framework
|
|
718
|
+
|
|
719
|
+
**Reasons**:
|
|
720
|
+
- **TypeScript First**: Better type inference
|
|
721
|
+
- **Performance**: Faster than Express
|
|
722
|
+
- **Modern**: Built for Edge/Serverless
|
|
723
|
+
- **Lightweight**: Smaller bundle size
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
### 6. Why TypeBox over Zod?
|
|
728
|
+
|
|
729
|
+
**Decision**: TypeBox for schema validation
|
|
730
|
+
|
|
731
|
+
**Reasons**:
|
|
732
|
+
- **JSON Schema**: Standard, interoperable
|
|
733
|
+
- **Performance**: Faster validation (JIT compilation)
|
|
734
|
+
- **OpenAPI**: Easy OpenAPI generation
|
|
735
|
+
- **Smaller**: Smaller bundle size
|
|
736
|
+
|
|
737
|
+
**Note**: Both are supported, TypeBox is the default
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## Extension Points
|
|
742
|
+
|
|
743
|
+
### 1. Custom Middleware
|
|
744
|
+
|
|
745
|
+
Add global or route-specific middleware:
|
|
746
|
+
|
|
352
747
|
```typescript
|
|
353
|
-
|
|
748
|
+
// server.config.ts
|
|
749
|
+
import { defineServerConfig } from '@spfn/core/server';
|
|
750
|
+
import { defineMiddleware } from '@spfn/core/route';
|
|
751
|
+
|
|
752
|
+
const rateLimitMiddleware = defineMiddleware('rateLimit', async (c, next) => {
|
|
753
|
+
const ip = c.req.header('x-forwarded-for');
|
|
754
|
+
if (await isRateLimited(ip)) {
|
|
755
|
+
return c.json({ error: 'Rate limit exceeded' }, 429);
|
|
756
|
+
}
|
|
757
|
+
await next();
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
export default defineServerConfig()
|
|
761
|
+
.middlewares([rateLimitMiddleware])
|
|
762
|
+
.build();
|
|
354
763
|
|
|
355
|
-
//
|
|
356
|
-
const
|
|
764
|
+
// Route can skip middleware
|
|
765
|
+
export const publicRoute = route.get('/public')
|
|
766
|
+
.middleware({ skip: ['rateLimit'] })
|
|
767
|
+
.handler(async (c) => { ... });
|
|
357
768
|
```
|
|
358
769
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
### 2. Custom Interceptors
|
|
773
|
+
|
|
774
|
+
Add request/response interceptors:
|
|
775
|
+
|
|
776
|
+
```typescript
|
|
777
|
+
// app/api/rpc/[routeName]/route.ts
|
|
778
|
+
import { appRouter } from '@/server/router';
|
|
779
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
780
|
+
|
|
781
|
+
export const { GET, POST } = createRpcProxy({
|
|
782
|
+
router: appRouter,
|
|
783
|
+
interceptors: [
|
|
784
|
+
{
|
|
785
|
+
pathPattern: '/admin/*',
|
|
786
|
+
request: async (ctx, next) => {
|
|
787
|
+
// Check admin role
|
|
788
|
+
const isAdmin = await checkAdminRole(ctx.cookies);
|
|
789
|
+
if (!isAdmin) {
|
|
790
|
+
throw new Error('Unauthorized');
|
|
791
|
+
}
|
|
792
|
+
await next();
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
});
|
|
366
797
|
```
|
|
367
798
|
|
|
368
|
-
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
### 3. Plugin System
|
|
802
|
+
|
|
803
|
+
Create SPFN packages with auto-discovery:
|
|
804
|
+
|
|
369
805
|
```typescript
|
|
370
|
-
//
|
|
371
|
-
|
|
806
|
+
// @yourcompany/spfn-analytics package
|
|
807
|
+
import { registerInterceptors } from '@spfn/core/nextjs/server';
|
|
808
|
+
|
|
809
|
+
registerInterceptors('analytics', [
|
|
810
|
+
{
|
|
811
|
+
pathPattern: '*',
|
|
812
|
+
response: async (ctx, next) => {
|
|
813
|
+
await analytics.track({
|
|
814
|
+
path: ctx.path,
|
|
815
|
+
status: ctx.response.status,
|
|
816
|
+
duration: Date.now() - ctx.metadata.startTime,
|
|
817
|
+
});
|
|
818
|
+
await next();
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
]);
|
|
822
|
+
|
|
823
|
+
// App: Auto-discovery
|
|
824
|
+
import '@yourcompany/spfn-analytics/adapters/nextjs';
|
|
825
|
+
export { GET, POST } from '@spfn/core/nextjs/server';
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
372
829
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
830
|
+
### 4. Custom Database Helpers
|
|
831
|
+
|
|
832
|
+
Extend database layer with domain-specific functions:
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
// src/server/repositories/users.repository.ts
|
|
836
|
+
import { findOne, create } from '@spfn/core/db';
|
|
837
|
+
import { users } from '../entities/users';
|
|
838
|
+
|
|
839
|
+
export async function findUserByEmail(email: string) {
|
|
840
|
+
return findOne(users, { email });
|
|
378
841
|
}
|
|
379
842
|
|
|
380
|
-
|
|
381
|
-
|
|
843
|
+
export async function createUserWithProfile(data) {
|
|
844
|
+
const user = await create(users, data.user);
|
|
845
|
+
const profile = await create(profiles, {
|
|
846
|
+
...data.profile,
|
|
847
|
+
userId: user.id,
|
|
848
|
+
});
|
|
849
|
+
return { user, profile };
|
|
850
|
+
}
|
|
382
851
|
```
|
|
383
852
|
|
|
384
|
-
|
|
385
|
-
Automatic transaction management with async context propagation.
|
|
853
|
+
---
|
|
386
854
|
|
|
387
|
-
|
|
855
|
+
## Migration Guides
|
|
388
856
|
|
|
389
|
-
|
|
390
|
-
- Auto-commit on success, auto-rollback on error
|
|
391
|
-
- AsyncLocalStorage-based context
|
|
392
|
-
- Transaction logging
|
|
857
|
+
### From Contract-Based to define-route
|
|
393
858
|
|
|
394
|
-
|
|
395
|
-
|
|
859
|
+
**Before** (Deprecated):
|
|
860
|
+
```typescript
|
|
861
|
+
// contract.ts
|
|
862
|
+
export const getUserContract = {
|
|
863
|
+
method: 'GET' as const,
|
|
864
|
+
path: '/users/:id',
|
|
865
|
+
params: Type.Object({ id: Type.String() }),
|
|
866
|
+
response: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
867
|
+
} as const satisfies RouteContract;
|
|
868
|
+
|
|
869
|
+
// route.ts
|
|
870
|
+
import { createApp } from '@spfn/core/route';
|
|
871
|
+
const app = createApp();
|
|
872
|
+
app.bind(getUserContract, async (c) => { ... });
|
|
873
|
+
export default app;
|
|
874
|
+
```
|
|
396
875
|
|
|
397
|
-
**
|
|
876
|
+
**After** (Current):
|
|
877
|
+
```typescript
|
|
878
|
+
// routes/users.ts
|
|
879
|
+
import { route } from '@spfn/core/route';
|
|
880
|
+
|
|
881
|
+
export const getUser = route.get('/users/:id')
|
|
882
|
+
.input({
|
|
883
|
+
params: Type.Object({ id: Type.String() }),
|
|
884
|
+
})
|
|
885
|
+
.handler(async (c) => {
|
|
886
|
+
const { params } = await c.data();
|
|
887
|
+
return c.success({ id: params.id, name: 'John' });
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// router.ts
|
|
891
|
+
import { defineRouter } from '@spfn/core/route';
|
|
892
|
+
export const appRouter = defineRouter({ getUser });
|
|
893
|
+
export type AppRouter = typeof appRouter;
|
|
894
|
+
```
|
|
398
895
|
|
|
399
|
-
|
|
400
|
-
|
|
896
|
+
**Benefits**:
|
|
897
|
+
- Better type inference
|
|
898
|
+
- Explicit imports (tree-shaking)
|
|
899
|
+
- No separate contract files
|
|
401
900
|
|
|
402
|
-
|
|
901
|
+
---
|
|
403
902
|
|
|
404
|
-
###
|
|
405
|
-
Request logging, CORS, and error handling middleware.
|
|
903
|
+
### From File-Based to define-route
|
|
406
904
|
|
|
407
|
-
**
|
|
905
|
+
**Before** (Deprecated):
|
|
906
|
+
```typescript
|
|
907
|
+
// routes/users/[id].ts
|
|
908
|
+
export default async function handler(c) { ... }
|
|
408
909
|
|
|
409
|
-
|
|
410
|
-
|
|
910
|
+
// Automatic file system scanning
|
|
911
|
+
```
|
|
411
912
|
|
|
412
|
-
**
|
|
913
|
+
**After** (Current):
|
|
914
|
+
```typescript
|
|
915
|
+
// routes/users.ts
|
|
916
|
+
export const getUser = route.get('/users/:id')...
|
|
413
917
|
|
|
414
|
-
|
|
415
|
-
|
|
918
|
+
// router.ts
|
|
919
|
+
export const appRouter = defineRouter({ getUser });
|
|
416
920
|
|
|
417
|
-
|
|
921
|
+
// server.config.ts
|
|
922
|
+
export default defineServerConfig()
|
|
923
|
+
.routes(appRouter)
|
|
924
|
+
.build();
|
|
925
|
+
```
|
|
418
926
|
|
|
419
|
-
**
|
|
420
|
-
-
|
|
421
|
-
-
|
|
422
|
-
-
|
|
423
|
-
- Configuration validation with clear error messages
|
|
424
|
-
- Multiple transports (Console, File, Slack, Email)
|
|
927
|
+
**Benefits**:
|
|
928
|
+
- Explicit route registration
|
|
929
|
+
- Better IDE support
|
|
930
|
+
- No runtime file system scanning
|
|
425
931
|
|
|
426
|
-
|
|
427
|
-
Automatic code generation with pluggable generators and centralized file watching.
|
|
932
|
+
---
|
|
428
933
|
|
|
429
|
-
|
|
934
|
+
### From ContractClient to tRPC-Style API
|
|
430
935
|
|
|
431
|
-
**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
936
|
+
**Before** (Old Client):
|
|
937
|
+
```typescript
|
|
938
|
+
import { ContractClient } from '@spfn/core/client';
|
|
939
|
+
import { getUserContract } from '@/contracts/users';
|
|
940
|
+
|
|
941
|
+
const client = new ContractClient({ baseUrl: 'http://localhost:8790' });
|
|
942
|
+
const user = await client.call(getUserContract, {
|
|
943
|
+
params: { id: '123' },
|
|
944
|
+
});
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
**After** (Current):
|
|
948
|
+
```typescript
|
|
949
|
+
import { api } from '@spfn/core/nextjs/server';
|
|
950
|
+
|
|
951
|
+
const user = await api.getUser
|
|
952
|
+
.params({ id: '123' })
|
|
953
|
+
.call();
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Benefits**:
|
|
957
|
+
- tRPC-style DX
|
|
958
|
+
- Method chaining
|
|
959
|
+
- Better type inference
|
|
960
|
+
- Automatic cookie handling
|
|
961
|
+
|
|
962
|
+
---
|
|
437
963
|
|
|
438
964
|
## Module Exports
|
|
439
965
|
|
|
440
|
-
### Main
|
|
966
|
+
### Main Server Exports
|
|
967
|
+
|
|
441
968
|
```typescript
|
|
442
|
-
import {
|
|
969
|
+
import {
|
|
970
|
+
startServer,
|
|
971
|
+
createServer,
|
|
972
|
+
defineServerConfig,
|
|
973
|
+
} from '@spfn/core';
|
|
443
974
|
```
|
|
444
975
|
|
|
445
|
-
###
|
|
976
|
+
### Route System
|
|
977
|
+
|
|
446
978
|
```typescript
|
|
447
|
-
import {
|
|
448
|
-
|
|
979
|
+
import {
|
|
980
|
+
route,
|
|
981
|
+
defineRouter,
|
|
982
|
+
} from '@spfn/core/route';
|
|
983
|
+
|
|
984
|
+
import type {
|
|
985
|
+
RouteDef,
|
|
986
|
+
Router,
|
|
987
|
+
RouteInput,
|
|
988
|
+
} from '@spfn/core/route';
|
|
449
989
|
```
|
|
450
990
|
|
|
451
|
-
### Database
|
|
991
|
+
### Database System
|
|
992
|
+
|
|
452
993
|
```typescript
|
|
453
994
|
import {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
995
|
+
getDatabase,
|
|
996
|
+
findOne,
|
|
997
|
+
findMany,
|
|
998
|
+
create,
|
|
999
|
+
createMany,
|
|
1000
|
+
updateOne,
|
|
1001
|
+
updateMany,
|
|
1002
|
+
deleteOne,
|
|
1003
|
+
deleteMany,
|
|
1004
|
+
count,
|
|
1005
|
+
} from '@spfn/core/db';
|
|
1006
|
+
|
|
1007
|
+
import {
|
|
1008
|
+
Transactional,
|
|
1009
|
+
getTransaction,
|
|
1010
|
+
runWithTransaction,
|
|
464
1011
|
} from '@spfn/core/db';
|
|
465
1012
|
```
|
|
466
1013
|
|
|
467
|
-
###
|
|
1014
|
+
### Client System
|
|
1015
|
+
|
|
468
1016
|
```typescript
|
|
1017
|
+
// Client-safe exports (works in Client Components)
|
|
469
1018
|
import {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1019
|
+
createApi,
|
|
1020
|
+
ApiError,
|
|
1021
|
+
} from '@spfn/core/nextjs';
|
|
1022
|
+
|
|
1023
|
+
// Server-only exports (API Routes)
|
|
1024
|
+
import {
|
|
1025
|
+
createRpcProxy,
|
|
1026
|
+
registerInterceptors,
|
|
1027
|
+
} from '@spfn/core/nextjs/server';
|
|
474
1028
|
```
|
|
475
1029
|
|
|
476
|
-
###
|
|
1030
|
+
### Error System
|
|
1031
|
+
|
|
477
1032
|
```typescript
|
|
478
|
-
import {
|
|
1033
|
+
import {
|
|
1034
|
+
ApiError,
|
|
1035
|
+
ValidationError,
|
|
1036
|
+
NotFoundError,
|
|
1037
|
+
ConflictError,
|
|
1038
|
+
UnauthorizedError,
|
|
1039
|
+
} from '@spfn/core/errors';
|
|
479
1040
|
```
|
|
480
1041
|
|
|
481
|
-
### Logger
|
|
1042
|
+
### Logger System
|
|
1043
|
+
|
|
482
1044
|
```typescript
|
|
483
1045
|
import { logger } from '@spfn/core';
|
|
484
1046
|
```
|
|
485
1047
|
|
|
486
|
-
###
|
|
1048
|
+
### Cache System
|
|
1049
|
+
|
|
487
1050
|
```typescript
|
|
488
|
-
import {
|
|
1051
|
+
import {
|
|
1052
|
+
initCache,
|
|
1053
|
+
getCache,
|
|
1054
|
+
getCacheRead,
|
|
1055
|
+
isCacheDisabled,
|
|
1056
|
+
closeCache,
|
|
1057
|
+
} from '@spfn/core/cache';
|
|
489
1058
|
```
|
|
490
1059
|
|
|
491
|
-
|
|
1060
|
+
### Environment System
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
import {
|
|
1064
|
+
defineEnvSchema,
|
|
1065
|
+
createEnvRegistry,
|
|
1066
|
+
envString,
|
|
1067
|
+
envNumber,
|
|
1068
|
+
envBoolean,
|
|
1069
|
+
envEnum,
|
|
1070
|
+
} from '@spfn/core/env';
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### Configuration System
|
|
1074
|
+
|
|
1075
|
+
```typescript
|
|
1076
|
+
import { env, envSchema } from '@spfn/core/config';
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
---
|
|
1080
|
+
|
|
1081
|
+
## Quick Reference
|
|
1082
|
+
|
|
1083
|
+
### Environment Variables
|
|
492
1084
|
|
|
493
1085
|
```bash
|
|
494
1086
|
# Database (required)
|
|
@@ -497,60 +1089,130 @@ DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
|
|
497
1089
|
# Database Read Replica (optional)
|
|
498
1090
|
DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
|
|
499
1091
|
|
|
500
|
-
# Redis (optional)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1092
|
+
# Cache - Valkey/Redis (optional)
|
|
1093
|
+
CACHE_URL=redis://localhost:6379
|
|
1094
|
+
CACHE_WRITE_URL=redis://master:6379
|
|
1095
|
+
CACHE_READ_URL=redis://replica:6379
|
|
1096
|
+
|
|
1097
|
+
# Next.js App URL (for Server Components calling API Routes)
|
|
1098
|
+
SPFN_APP_URL=http://localhost:3000
|
|
1099
|
+
|
|
1100
|
+
# SPFN API Server URL (for API Route Proxy)
|
|
1101
|
+
SPFN_API_URL=http://localhost:8790
|
|
504
1102
|
|
|
505
1103
|
# Server
|
|
506
1104
|
PORT=8790
|
|
507
1105
|
HOST=localhost
|
|
508
1106
|
NODE_ENV=development
|
|
509
1107
|
|
|
510
|
-
# Server Timeouts (optional,
|
|
511
|
-
SERVER_TIMEOUT=120000
|
|
512
|
-
SERVER_KEEPALIVE_TIMEOUT=65000
|
|
513
|
-
SERVER_HEADERS_TIMEOUT=60000
|
|
514
|
-
SHUTDOWN_TIMEOUT=30000
|
|
1108
|
+
# Server Timeouts (optional, milliseconds)
|
|
1109
|
+
SERVER_TIMEOUT=120000
|
|
1110
|
+
SERVER_KEEPALIVE_TIMEOUT=65000
|
|
1111
|
+
SERVER_HEADERS_TIMEOUT=60000
|
|
1112
|
+
SHUTDOWN_TIMEOUT=30000
|
|
515
1113
|
|
|
516
1114
|
# Logger (optional)
|
|
517
|
-
LOGGER_ADAPTER=pino
|
|
518
|
-
LOGGER_FILE_ENABLED=true
|
|
519
|
-
LOG_DIR=/var/log/myapp
|
|
1115
|
+
LOGGER_ADAPTER=pino
|
|
1116
|
+
LOGGER_FILE_ENABLED=true
|
|
1117
|
+
LOG_DIR=/var/log/myapp
|
|
520
1118
|
```
|
|
521
1119
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
- Node.js >= 18
|
|
525
|
-
- Next.js 15+ with App Router (when using with CLI)
|
|
526
|
-
- PostgreSQL
|
|
527
|
-
- Redis (optional)
|
|
1120
|
+
---
|
|
528
1121
|
|
|
529
|
-
|
|
1122
|
+
### Basic Setup
|
|
530
1123
|
|
|
1124
|
+
**1. Install**
|
|
531
1125
|
```bash
|
|
532
|
-
npm
|
|
533
|
-
|
|
534
|
-
|
|
1126
|
+
npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
**2. Define Routes**
|
|
1130
|
+
```typescript
|
|
1131
|
+
// src/server/routes/users.ts
|
|
1132
|
+
export const getUser = route.get('/users/:id')
|
|
1133
|
+
.input({ params: Type.Object({ id: Type.String() }) })
|
|
1134
|
+
.handler(async (c) => {
|
|
1135
|
+
const { params } = await c.data();
|
|
1136
|
+
return c.success({ id: params.id, name: 'John' });
|
|
1137
|
+
});
|
|
535
1138
|
```
|
|
536
1139
|
|
|
537
|
-
**
|
|
1140
|
+
**3. Create Router**
|
|
1141
|
+
```typescript
|
|
1142
|
+
// src/server/router.ts
|
|
1143
|
+
export const appRouter = defineRouter({ getUser });
|
|
1144
|
+
export type AppRouter = typeof appRouter;
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
**4. Configure Server**
|
|
1148
|
+
```typescript
|
|
1149
|
+
// src/server/server.config.ts
|
|
1150
|
+
export default defineServerConfig()
|
|
1151
|
+
.port(8790)
|
|
1152
|
+
.routes(appRouter)
|
|
1153
|
+
.build();
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
**5. Create RPC Proxy (Next.js)**
|
|
1157
|
+
```typescript
|
|
1158
|
+
// app/api/rpc/[routeName]/route.ts
|
|
1159
|
+
import { appRouter } from '@/server/router';
|
|
1160
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
1161
|
+
|
|
1162
|
+
export const { GET, POST } = createRpcProxy({ router: appRouter });
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
**6. Use Client**
|
|
1166
|
+
```typescript
|
|
1167
|
+
// app/users/[id]/page.tsx
|
|
1168
|
+
import { createApi } from '@spfn/core/nextjs';
|
|
1169
|
+
import type { AppRouter } from '@/server/router';
|
|
1170
|
+
|
|
1171
|
+
const api = createApi<AppRouter>();
|
|
1172
|
+
const user = await api.getUser.call({ params: { id: '123' } });
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
---
|
|
538
1176
|
|
|
539
1177
|
## Documentation
|
|
540
1178
|
|
|
1179
|
+
### Technical Architecture
|
|
1180
|
+
- [Route System](./src/route/README.md) - define-route system, type inference
|
|
1181
|
+
- [Server System](./src/server/README.md) - Configuration, middleware pipeline, lifecycle
|
|
1182
|
+
- [Database System](./src/db/README.md) - Helper functions, transactions, schema helpers
|
|
1183
|
+
- [Client System](./src/nextjs/README.md) - tRPC-style API, TypedProxy, interceptors
|
|
1184
|
+
|
|
541
1185
|
### Guides
|
|
542
|
-
- [File-based Routing](./src/route/README.md)
|
|
543
|
-
- [Database & Helper Functions](./src/db/README.md)
|
|
544
1186
|
- [Transaction Management](./src/db/docs/transactions.md)
|
|
545
|
-
- [Redis Cache](./src/cache/README.md)
|
|
546
1187
|
- [Error Handling](./src/errors/README.md)
|
|
547
|
-
- [Middleware](./src/middleware/README.md)
|
|
548
|
-
- [Server Configuration](./src/server/README.md)
|
|
549
1188
|
- [Logger](./src/logger/README.md)
|
|
1189
|
+
- [Cache](./src/cache/README.md)
|
|
1190
|
+
- [Middleware](./src/middleware/README.md)
|
|
550
1191
|
- [Code Generation](./src/codegen/README.md)
|
|
551
1192
|
|
|
552
|
-
|
|
553
|
-
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
## Requirements
|
|
1196
|
+
|
|
1197
|
+
- Node.js >= 18
|
|
1198
|
+
- Next.js 15+ with App Router (when using client integration)
|
|
1199
|
+
- PostgreSQL
|
|
1200
|
+
- Valkey/Redis (optional)
|
|
1201
|
+
- TypeScript >= 5.0
|
|
1202
|
+
|
|
1203
|
+
---
|
|
1204
|
+
|
|
1205
|
+
## Testing
|
|
1206
|
+
|
|
1207
|
+
```bash
|
|
1208
|
+
npm test # Run all tests
|
|
1209
|
+
npm test -- route # Run route tests only
|
|
1210
|
+
npm test -- --coverage # With coverage
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
**Test Coverage**: 650+ tests across all modules
|
|
1214
|
+
|
|
1215
|
+
---
|
|
554
1216
|
|
|
555
1217
|
## License
|
|
556
1218
|
|