@spfn/core 0.2.0-beta.5 → 0.2.0-beta.51
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/LICENSE +1 -1
- package/README.md +181 -1281
- package/dist/{boss-BO8ty33K.d.ts → boss-Cxqc-Oiw.d.ts} +37 -7
- package/dist/cache/index.js +32 -29
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +179 -5
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +168 -6
- package/dist/config/index.js +29 -5
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +218 -4
- package/dist/db/index.js +351 -57
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +11 -2
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +17 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +188 -20
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +213 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +23 -8
- package/dist/job/index.js +154 -44
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +23 -1
- package/dist/middleware/index.js +58 -5
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +77 -31
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +44 -23
- package/dist/nextjs/server.js +83 -65
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +158 -4
- package/dist/route/index.js +238 -22
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +308 -17
- package/dist/server/index.js +1128 -261
- package/dist/server/index.js.map +1 -1
- package/dist/{router-Di7ENoah.d.ts → token-manager-CyG7la3p.d.ts} +116 -1
- package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
- package/dist/types-C1jMLGwK.d.ts +257 -0
- package/dist/types-Cfj--lfr.d.ts +151 -0
- package/docs/file-upload.md +717 -0
- package/package.json +18 -5
- package/dist/types-B-e_f2dQ.d.ts +0 -121
package/README.md
CHANGED
|
@@ -1,1308 +1,208 @@
|
|
|
1
|
-
# @spfn/core -
|
|
1
|
+
# @spfn/core — Type-safe Next.js + Hono backend framework (route DSL → RPC proxy → typed client)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@spfn/core` is the backend runtime for SPFN: a tRPC-style **route DSL** (TypeBox-validated,
|
|
4
|
+
end-to-end typed), a Drizzle/postgres.js **data layer**, an HTTP **server** entry point, and a
|
|
5
|
+
Next.js **RPC proxy + typed client** that wires a browser/RSC app to that backend with
|
|
6
|
+
compile-time-only type inference.
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
There is **no root barrel** — `@spfn/core` itself imports nothing. Everything is reached
|
|
9
|
+
through subpath exports (`@spfn/core/route`, `@spfn/core/db`, `@spfn/core/nextjs`, …). Each
|
|
10
|
+
module is canonically documented in its own `src/<mod>/README.md`; this file is the index +
|
|
11
|
+
end-to-end flow. Follow the links for API detail.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Install
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
+---------------------------------------------------------------+
|
|
142
|
-
```
|
|
143
|
-
|
|
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
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## Module Architecture
|
|
188
|
-
|
|
189
|
-
### 1. Route System (`src/route/`)
|
|
190
|
-
|
|
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: Direct return / c.json() / helpers
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**Key Components**:
|
|
207
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
**Design Pattern**: Builder pattern with type inference
|
|
214
|
-
|
|
215
|
-
**[→ Full Documentation](./src/route/README.md)**
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
### 2. Server System (`src/server/`)
|
|
220
|
-
|
|
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
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
**Key Components**:
|
|
260
|
-
|
|
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/`)
|
|
274
|
-
|
|
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)
|
|
297
|
-
```
|
|
298
|
-
|
|
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**:
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
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
|
-
```
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
### 4. Client System (`src/nextjs/`)
|
|
328
|
-
|
|
329
|
-
**Purpose**: Type-safe API client for Next.js with tRPC-style DX
|
|
330
|
-
|
|
331
|
-
**Architecture**:
|
|
332
|
-
|
|
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
|
|
429
|
-
|
|
430
|
-
**[→ Full Documentation](./src/middleware/README.md)**
|
|
431
|
-
|
|
432
|
-
---
|
|
433
|
-
|
|
434
|
-
### 9. Job System (`src/job/`)
|
|
435
|
-
|
|
436
|
-
**Purpose**: Background job processing with pg-boss
|
|
437
|
-
|
|
438
|
-
**Architecture**:
|
|
439
|
-
|
|
440
|
-
```
|
|
441
|
-
job('send-email')
|
|
442
|
-
.input(schema)
|
|
443
|
-
.options({ retryLimit: 3 })
|
|
444
|
-
.cron('0 9 * * *')
|
|
445
|
-
.on(eventDef)
|
|
446
|
-
.handler(async (input) => { ... })
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**Key Components**:
|
|
450
|
-
|
|
451
|
-
- `job()`: Fluent job builder
|
|
452
|
-
- `defineJobRouter()`: Group jobs for registration
|
|
453
|
-
- Job types: Standard, Cron, RunOnce, Event-driven
|
|
454
|
-
- `initBoss()`, `registerJobs()`: pg-boss lifecycle
|
|
455
|
-
|
|
456
|
-
**Design Pattern**: Builder pattern (same as routes)
|
|
457
|
-
|
|
458
|
-
---
|
|
459
|
-
|
|
460
|
-
### 10. Event System (`src/event/`)
|
|
461
|
-
|
|
462
|
-
**Purpose**: Decoupled pub/sub event system
|
|
463
|
-
|
|
464
|
-
**Architecture**:
|
|
465
|
-
|
|
466
|
-
```
|
|
467
|
-
defineEvent('user.created', schema)
|
|
468
|
-
→ emit(payload)
|
|
469
|
-
→ handlers (in-memory)
|
|
470
|
-
→ job queues (pg-boss)
|
|
471
|
-
→ cache pub/sub (multi-instance)
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
**Key Components**:
|
|
475
|
-
|
|
476
|
-
- `defineEvent()`: Define typed events
|
|
477
|
-
- `event.emit()`: Emit event to all subscribers
|
|
478
|
-
- `event.subscribe()`: In-memory subscription
|
|
479
|
-
- `event.useCache()`: Multi-instance pub/sub via Redis
|
|
480
|
-
|
|
481
|
-
**Integration**: `job().on(event)` for event-driven jobs
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
## Type System
|
|
486
|
-
|
|
487
|
-
### End-to-End Type Safety Flow
|
|
488
|
-
|
|
489
|
-
```typescript
|
|
490
|
-
// 1. Database Schema (Source of Truth)
|
|
491
|
-
// src/server/entities/users.ts
|
|
492
|
-
export const users = pgTable('users', {
|
|
493
|
-
id: id(),
|
|
494
|
-
name: text('name').notNull(),
|
|
495
|
-
email: text('email').notNull().unique(),
|
|
496
|
-
...timestamps(),
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
export type User = typeof users.$inferSelect;
|
|
500
|
-
// ^? { id: string; name: string; email: string; createdAt: Date; updatedAt: Date }
|
|
501
|
-
|
|
502
|
-
// 2. Server Route Definition
|
|
503
|
-
// src/server/routes/users.ts
|
|
504
|
-
import { route } from '@spfn/core/route';
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @spfn/core
|
|
17
|
+
# peer (optional): next ^15 || ^16
|
|
18
|
+
# optional deps: ioredis (cache), ws (websocket events); pg-boss ships as a direct dep for jobs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Node `>=18.18.0`. ESM-only.
|
|
22
|
+
|
|
23
|
+
> **Apps that define entities must declare the Postgres driver directly:** add
|
|
24
|
+
> `postgres` (and `pg`) to your app's `dependencies`. `@spfn/core` uses postgres.js
|
|
25
|
+
> internally, and Drizzle branches its type resolution on the `postgres`/`pg` peer. If
|
|
26
|
+
> the app doesn't pin the same driver, pnpm resolves `drizzle-orm` to a second instance,
|
|
27
|
+
> `BaseRepository` generics collapse to `unknown`, and RPC responses lose their types.
|
|
28
|
+
> `spfn create` adds these automatically; declare them by hand only when wiring SPFN into
|
|
29
|
+
> an existing app.
|
|
30
|
+
>
|
|
31
|
+
> ```bash
|
|
32
|
+
> pnpm add postgres pg
|
|
33
|
+
> ```
|
|
34
|
+
|
|
35
|
+
## Modules
|
|
36
|
+
|
|
37
|
+
Each entry below is exactly one subpath from `package.json` `exports`. Import path → one-line
|
|
38
|
+
purpose → canonical README.
|
|
39
|
+
|
|
40
|
+
| Import path | Purpose | Doc |
|
|
41
|
+
|-------------|---------|-----|
|
|
42
|
+
| `@spfn/core/route` | tRPC-style route DSL (`route.get(...).input(...).handler(...)`) + `defineRouter` / `registerRoutes` / `defineMiddleware`. The core. | [src/route/README.md](./src/route/README.md) |
|
|
43
|
+
| `@spfn/core/route/types` | Shared route types (`HttpMethod`, route/router type primitives). | [src/route/README.md](./src/route/README.md) |
|
|
44
|
+
| `@spfn/core/server` | HTTP server entry: `defineServerConfig()` → `startServer()`; middleware auto-wiring, infra init, graceful shutdown. | [src/server/README.md](./src/server/README.md) |
|
|
45
|
+
| `@spfn/core/nextjs` | Client-safe: `createApi<AppRouter>()`, `ApiError`, client types. No `next/headers`. | [src/nextjs/README.md](./src/nextjs/README.md) |
|
|
46
|
+
| `@spfn/core/nextjs/server` | Server-only: `createRpcProxy({ routeMap })`, `registerInterceptors`. Uses `next/headers`. | [src/nextjs/README.md](./src/nextjs/README.md) |
|
|
47
|
+
| `@spfn/core/db` | Type-safe PostgreSQL (Drizzle): CRUD helpers, `BaseRepository`, schema helpers, transactions, PG error mapping. Single entry point. | [src/db/README.md](./src/db/README.md) |
|
|
48
|
+
| `@spfn/core/db` → manager | Connection lifecycle, pool, primary/replica, health-check, reconnect (`initDatabase`, `getDatabase`). Re-exported from `@spfn/core/db`. | [src/db/manager/README.md](./src/db/manager/README.md) |
|
|
49
|
+
| `@spfn/core/db` → schema | Drizzle column helpers (`id`, `uuid`, `timestamps`, `foreignKey`, `enumText`, `typedJsonb`, `softDelete`, …). Re-exported from `@spfn/core/db`. | [src/db/schema/README.md](./src/db/schema/README.md) |
|
|
50
|
+
| `@spfn/core/db` → transaction | `Transactional` middleware + `runInTransaction`; AsyncLocalStorage propagation to every repo. Re-exported from `@spfn/core/db`. | [src/db/transaction/README.md](./src/db/transaction/README.md) |
|
|
51
|
+
| `@spfn/core/middleware` | Built-in Hono middleware: `ErrorHandler`, `RequestLogger` (+ masking helper). | [src/middleware/README.md](./src/middleware/README.md) |
|
|
52
|
+
| `@spfn/core/errors` | Serializable HTTP/DB error classes + `ErrorRegistry` for cross-boundary deserialization. | [src/errors/README.md](./src/errors/README.md) |
|
|
53
|
+
| `@spfn/core/env` | Schema-based env validation/parsing (isomorphic). `@spfn/core/env/loader` is the **server-only** file loader (`node:fs`). | [src/env/README.md](./src/env/README.md) |
|
|
54
|
+
| `@spfn/core/config` | `@spfn/core`'s own validated env config (`env`, `envSchema`, `registry`) built on `@spfn/core/env`. | [src/config/README.md](./src/config/README.md) |
|
|
55
|
+
| `@spfn/core/logger` | Zero-dependency structured singleton `logger` + child loggers, level masking. | [src/logger/README.md](./src/logger/README.md) |
|
|
56
|
+
| `@spfn/core/cache` | Singleton Valkey/Redis (ioredis) manager, graceful-degrading (`getCache`, `getCacheRead`). | [src/cache/README.md](./src/cache/README.md) |
|
|
57
|
+
| `@spfn/core/job` | pg-boss background jobs: fluent `job()` builder, cron/run-once/event-driven, `defineJobRouter`. | [src/job/README.md](./src/job/README.md) |
|
|
58
|
+
| `@spfn/core/event` | Decoupled pub/sub (`defineEvent`, `defineEventRouter`, `eventRouteMap`); SSE/WS variants below. | [src/event/README.md](./src/event/README.md) |
|
|
59
|
+
| `@spfn/core/event/sse` | Server SSE handler + token manager (SERVER ONLY). | [src/event/README.md](./src/event/README.md) |
|
|
60
|
+
| `@spfn/core/event/sse/client` | Browser SSE (`EventSource`) client. | [src/event/README.md](./src/event/README.md) |
|
|
61
|
+
| `@spfn/core/event/ws` | Server WebSocket handler (SERVER ONLY; `ws` optional dep). | [src/event/README.md](./src/event/README.md) |
|
|
62
|
+
| `@spfn/core/event/ws/client` | Browser WebSocket client. | [src/event/README.md](./src/event/README.md) |
|
|
63
|
+
| `@spfn/core/codegen` | Pluggable codegen orchestrator + the built-in `@spfn/core:route-map` generator that produces the proxy's `routeMap`. | [src/codegen/README.md](./src/codegen/README.md) |
|
|
64
|
+
|
|
65
|
+
`db/manager`, `db/schema`, `db/transaction` have **no package subpath** of their own — they are
|
|
66
|
+
internal modules re-exported by `@spfn/core/db`. Import them from `@spfn/core/db`. (See Pitfalls
|
|
67
|
+
for the `./client` subpath.)
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
End-to-end, a SPFN backend is **defined once** on the server and consumed type-safely from the
|
|
72
|
+
Next.js app. Types flow purely through TypeScript inference; the only generated artifact is the
|
|
73
|
+
proxy's `routeMap`.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
① route DSL ② defineRouter ③ defineServerConfig → startServer
|
|
77
|
+
route.get('/users/:id') defineRouter({ getUser, defineServerConfig()
|
|
78
|
+
.input({ params }) createUser }) .routes(appRouter).build()
|
|
79
|
+
.handler(c => …) export type AppRouter startServer() → Hono on :8790
|
|
80
|
+
│ = typeof appRouter ▲
|
|
81
|
+
│ TypeBox = runtime validation + compile-time types │ registerRoutes mounts routes
|
|
82
|
+
▼ │
|
|
83
|
+
④ codegen (@spfn/core:route-map) ──► routeMap = { getUser: { method:'GET', path:'/users/:id' }, … }
|
|
84
|
+
│
|
|
85
|
+
▼
|
|
86
|
+
⑤ Next.js RPC proxy ⑥ typed client
|
|
87
|
+
app/api/rpc/[routeName]/route.ts lib/api.ts
|
|
88
|
+
createRpcProxy({ routeMap }) createApi<AppRouter>() (no codegen for the client)
|
|
89
|
+
GET/POST /api/rpc/{routeName} api.getUser.call({ params:{ id } })
|
|
90
|
+
resolves real method+path from routeMap └─ fully typed input + output
|
|
91
|
+
forwards to backend, runs interceptors
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
1. **Define a route** with `route.<method>(path).input({...}).handler(c => …)` from
|
|
95
|
+
`@spfn/core/route`. TypeBox schemas in `.input()` give both runtime validation and the
|
|
96
|
+
compile-time types the handler (`await c.data()`) and the client see. The handler's **return
|
|
97
|
+
type is inferred** — no manual response typing.
|
|
98
|
+
2. **Compose routes** with `defineRouter({ … })` and export `type AppRouter = typeof appRouter`.
|
|
99
|
+
That type is the single source of truth for the client; no client codegen reads it.
|
|
100
|
+
3. **Boot the server** with `defineServerConfig().routes(appRouter).build()` then `startServer()`
|
|
101
|
+
(`@spfn/core/server`). It auto-wires `ErrorHandler`/`RequestLogger`, inits DB/cache, mounts
|
|
102
|
+
routes via `registerRoutes`, and runs jobs/events. A request is matched by Hono → global
|
|
103
|
+
middleware → route middleware (`.use([...])`, e.g. `Transactional()`) → input validation/type
|
|
104
|
+
conversion → handler → serialized response.
|
|
105
|
+
4. **Generate the route-map** with codegen (`@spfn/core:route-map`, e.g. `pnpm codegen`). It emits
|
|
106
|
+
`routeName → { method, path }` — the only thing the proxy needs to resolve the *real* backend
|
|
107
|
+
method/path.
|
|
108
|
+
5. **Mount the RPC proxy** in Next.js: `createRpcProxy({ routeMap })` from
|
|
109
|
+
`@spfn/core/nextjs/server` as the `app/api/rpc/[routeName]/route.ts` catch-all. The client only
|
|
110
|
+
ever sends `GET` (no body) or `POST` (body/formData) to `/api/rpc/{routeName}`; the proxy looks
|
|
111
|
+
up `routeMap[routeName]`, substitutes `:params`, and forwards to the backend with the resolved
|
|
112
|
+
method. Package route maps (`authRouteMap`, `eventRouteMap`) merge into the same `routeMap`.
|
|
113
|
+
6. **Call it** through `createApi<AppRouter>()` from `@spfn/core/nextjs`. The client is a `Proxy`
|
|
114
|
+
over `AppRouter` — `api.getUser.call({ params })` is fully typed in and out, with zero runtime
|
|
115
|
+
type cost. Errors come back as `ApiError`, or as the original typed error when its class is in
|
|
116
|
+
the client's `errorRegistry` (the core registry is always merged in).
|
|
117
|
+
|
|
118
|
+
## Quick example
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// server/router.ts — ① + ②
|
|
122
|
+
import { defineRouter, route } from '@spfn/core/route';
|
|
123
|
+
import { Transactional } from '@spfn/core/db';
|
|
505
124
|
import { Type } from '@sinclair/typebox';
|
|
506
125
|
|
|
507
|
-
export const getUser = route.get('/users/:id')
|
|
508
|
-
.input({
|
|
509
|
-
params: Type.Object({
|
|
510
|
-
id: Type.String(),
|
|
511
|
-
}),
|
|
512
|
-
query: Type.Object({
|
|
513
|
-
include: Type.Optional(Type.String()),
|
|
514
|
-
}),
|
|
515
|
-
})
|
|
516
|
-
.handler(async (c) => {
|
|
517
|
-
const { params, query } = await c.data();
|
|
518
|
-
// ^? { params: { id: string }, query: { include?: string } }
|
|
519
|
-
|
|
520
|
-
const user = await findOne(users, { id: params.id });
|
|
521
|
-
// ^? User | null
|
|
522
|
-
|
|
523
|
-
return user;
|
|
524
|
-
// ^? Response type inferred from return value
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// 3. Router Type Export
|
|
528
|
-
// src/server/router.ts
|
|
529
126
|
export const appRouter = defineRouter({
|
|
530
|
-
getUser
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// 4. Client API Call (Next.js)
|
|
538
|
-
// app/users/[id]/page.tsx
|
|
539
|
-
import { api } from '@spfn/core/nextjs/server';
|
|
540
|
-
|
|
541
|
-
const user = await api.getUser
|
|
542
|
-
.params({ id: '123' }) // Type-checked: must be { id: string }
|
|
543
|
-
.query({ include: 'posts' }) // Type-checked: { include?: string }
|
|
544
|
-
.call();
|
|
545
|
-
// ^? User (inferred from server handler return type)
|
|
546
|
-
|
|
547
|
-
// Full type inference chain:
|
|
548
|
-
// User type → RouteDef → Router → Client API → return type
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### Type Inference Utilities
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
// Extract input type from route
|
|
555
|
-
type GetUserInput = InferRouteInput<typeof getUser>;
|
|
556
|
-
// ^? { params: { id: string }; query: { include?: string } }
|
|
557
|
-
|
|
558
|
-
// Extract output type from route
|
|
559
|
-
type GetUserOutput = InferRouteOutput<typeof getUser>;
|
|
560
|
-
// ^? User
|
|
561
|
-
|
|
562
|
-
// Client type inference
|
|
563
|
-
type ApiClient<TRouter> = {
|
|
564
|
-
[K in keyof TRouter['routes']]: TRouter['routes'][K] extends RouteDef<infer TInput, infer TOutput>
|
|
565
|
-
? RouteClient<TInput, TOutput>
|
|
566
|
-
: never;
|
|
567
|
-
};
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
---
|
|
571
|
-
|
|
572
|
-
## Integration Points
|
|
573
|
-
|
|
574
|
-
### 1. Server ↔ Database: Transaction Context
|
|
575
|
-
|
|
576
|
-
**Mechanism**: AsyncLocalStorage propagates transaction across async calls
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
// Route handler
|
|
580
|
-
app.bind(createPostContract, [Transactional()], async (c) => {
|
|
581
|
-
// AsyncLocalStorage stores transaction
|
|
582
|
-
const post = await create(posts, { title: 'Hello' });
|
|
583
|
-
|
|
584
|
-
// Same transaction is used automatically
|
|
585
|
-
const tag = await create(tags, { postId: post.id, name: 'news' });
|
|
586
|
-
|
|
587
|
-
// Auto-commit on success, auto-rollback on error
|
|
588
|
-
return c.json(post);
|
|
589
|
-
});
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
**Why**: No need to pass transaction object through function calls
|
|
593
|
-
|
|
594
|
-
---
|
|
595
|
-
|
|
596
|
-
### 2. Server ↔ Client: Type Inference
|
|
597
|
-
|
|
598
|
-
**Mechanism**: `typeof appRouter` captures all route types
|
|
599
|
-
|
|
600
|
-
```typescript
|
|
601
|
-
// Server: Export router type
|
|
602
|
-
export const appRouter = defineRouter({ getUser, createUser });
|
|
603
|
-
export type AppRouter = typeof appRouter;
|
|
604
|
-
|
|
605
|
-
// Client: Import type (not value!)
|
|
606
|
-
import type { AppRouter } from '@/server/router';
|
|
607
|
-
const api = createApi<AppRouter>(/* ... */);
|
|
608
|
-
|
|
609
|
-
// Automatic type inference for all routes
|
|
610
|
-
const user = await api.getUser.params({ id: '123' }).call();
|
|
611
|
-
// ^? Full type safety without manual type definitions
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
**Why**: Single source of truth (server) → client types automatically sync
|
|
615
|
-
|
|
616
|
-
---
|
|
617
|
-
|
|
618
|
-
### 3. Next.js ↔ SPFN: RPC Proxy
|
|
619
|
-
|
|
620
|
-
**Mechanism**: Next.js API Route resolves routeName and forwards to SPFN server
|
|
621
|
-
|
|
622
|
-
```typescript
|
|
623
|
-
// app/api/rpc/[routeName]/route.ts
|
|
624
|
-
import { appRouter } from '@/server/router';
|
|
625
|
-
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
626
|
-
|
|
627
|
-
export const { GET, POST } = createRpcProxy({ router: appRouter });
|
|
628
|
-
|
|
629
|
-
// Automatically:
|
|
630
|
-
// 1. Resolves routeName → method/path from router
|
|
631
|
-
// 2. Forwards to http://localhost:8790/{resolved-path}
|
|
632
|
-
// 3. Applies interceptors (auth, cookies)
|
|
633
|
-
// 4. Returns NextResponse
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
**Why**:
|
|
637
|
-
- HttpOnly cookie support (browser → Next.js includes cookies automatically)
|
|
638
|
-
- Security (SPFN_API_URL hidden from browser)
|
|
639
|
-
- No CORS (same-origin requests)
|
|
640
|
-
- No metadata codegen required
|
|
641
|
-
|
|
642
|
-
---
|
|
643
|
-
|
|
644
|
-
### 4. Packages: Registry System
|
|
645
|
-
|
|
646
|
-
**Mechanism**: Packages register interceptors on import
|
|
647
|
-
|
|
648
|
-
```typescript
|
|
649
|
-
// @spfn/auth package
|
|
650
|
-
import { registerInterceptors } from '@spfn/core/nextjs/server';
|
|
651
|
-
|
|
652
|
-
registerInterceptors('auth', [
|
|
653
|
-
{
|
|
654
|
-
pathPattern: '/_auth/*',
|
|
655
|
-
response: async (ctx, next) => {
|
|
656
|
-
// Set session cookie
|
|
657
|
-
ctx.setCookies.push({ name: 'session', value: token });
|
|
658
|
-
await next();
|
|
659
|
-
},
|
|
660
|
-
},
|
|
661
|
-
]);
|
|
662
|
-
|
|
663
|
-
// App: Auto-discovery
|
|
664
|
-
import '@spfn/auth/adapters/nextjs'; // Registers interceptors
|
|
665
|
-
export { GET, POST } from '@spfn/core/nextjs/server'; // Uses registered interceptors
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
**Why**: Zero-config integration for plugin packages
|
|
669
|
-
|
|
670
|
-
---
|
|
671
|
-
|
|
672
|
-
## Design Decisions
|
|
673
|
-
|
|
674
|
-
### 1. Why tRPC-Style API over REST Client?
|
|
675
|
-
|
|
676
|
-
**Decision**: Method chaining with `.params().query().call()`
|
|
677
|
-
|
|
678
|
-
**Reasons**:
|
|
679
|
-
- Better DX: Fluent API guides usage
|
|
680
|
-
- Type safety: Each method is type-checked
|
|
681
|
-
- Flexibility: Optional parameters can be omitted
|
|
682
|
-
- Discovery: IDE autocomplete shows available options
|
|
683
|
-
|
|
684
|
-
**Example**:
|
|
685
|
-
```typescript
|
|
686
|
-
// tRPC-style (SPFN)
|
|
687
|
-
const user = await api.getUser
|
|
688
|
-
.params({ id: '123' })
|
|
689
|
-
.query({ include: 'posts' })
|
|
690
|
-
.call();
|
|
691
|
-
|
|
692
|
-
// vs Traditional REST client
|
|
693
|
-
const user = await client.get('/users/123', {
|
|
694
|
-
params: { include: 'posts' } // No type safety
|
|
695
|
-
});
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
---
|
|
699
|
-
|
|
700
|
-
### 2. Why API Route Proxy Pattern?
|
|
701
|
-
|
|
702
|
-
**Decision**: Next.js API Route forwards to SPFN server
|
|
703
|
-
|
|
704
|
-
**Reasons**:
|
|
705
|
-
- **HttpOnly Cookies**: Browser automatically sends cookies to same-origin
|
|
706
|
-
- **Security**: SPFN API URL hidden from browser
|
|
707
|
-
- **No CORS**: Same-origin requests (localhost:3000 → localhost:3000)
|
|
708
|
-
- **Unified Path**: Server Components and Client Components use same API
|
|
709
|
-
|
|
710
|
-
**Alternative Rejected**: Direct calls from browser to SPFN server
|
|
711
|
-
- Would expose SPFN_API_URL to browser
|
|
712
|
-
- CORS configuration required
|
|
713
|
-
- Cannot use HttpOnly cookies from browser
|
|
714
|
-
|
|
715
|
-
---
|
|
716
|
-
|
|
717
|
-
### 3. Why define-route over File-Based Routing?
|
|
718
|
-
|
|
719
|
-
**Decision**: Explicit route imports with `defineRouter()`
|
|
720
|
-
|
|
721
|
-
**Reasons**:
|
|
722
|
-
- **Explicit Imports**: Better tree-shaking, clear dependencies
|
|
723
|
-
- **Type Safety**: `typeof appRouter` captures all routes
|
|
724
|
-
- **Flexibility**: Routes can be defined anywhere
|
|
725
|
-
- **No Magic**: No file system scanning at runtime
|
|
726
|
-
|
|
727
|
-
**Alternative Rejected**: File-based routing (like Next.js)
|
|
728
|
-
- Runtime file system scanning
|
|
729
|
-
- Implicit route registration
|
|
730
|
-
- Harder to trace route definitions
|
|
731
|
-
- Less flexible for monorepo setups
|
|
732
|
-
|
|
733
|
-
**Migration Path**: Deprecated contract-based and file-based routing systems
|
|
734
|
-
|
|
735
|
-
---
|
|
736
|
-
|
|
737
|
-
### 4. Why AsyncLocalStorage for Transactions?
|
|
738
|
-
|
|
739
|
-
**Decision**: Transaction propagation without explicit passing
|
|
740
|
-
|
|
741
|
-
**Reasons**:
|
|
742
|
-
- **Clean API**: No need to pass `tx` object through all functions
|
|
743
|
-
- **Automatic**: Works across async boundaries
|
|
744
|
-
- **Safe**: Isolated per request
|
|
745
|
-
- **Compatible**: Works with existing code
|
|
746
|
-
|
|
747
|
-
**Example**:
|
|
748
|
-
```typescript
|
|
749
|
-
// With AsyncLocalStorage (SPFN)
|
|
750
|
-
async function createPostWithTags(data) {
|
|
751
|
-
const post = await create(posts, data);
|
|
752
|
-
const tags = await createMany(tags, data.tags.map(t => ({ postId: post.id, ...t })));
|
|
753
|
-
// Same transaction automatically
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// vs Explicit transaction passing
|
|
757
|
-
async function createPostWithTags(tx, data) {
|
|
758
|
-
const post = await tx.insert(posts).values(data);
|
|
759
|
-
const tags = await tx.insert(tags).values(...);
|
|
760
|
-
// Must pass tx everywhere
|
|
761
|
-
}
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
---
|
|
765
|
-
|
|
766
|
-
### 5. Why Hono over Express?
|
|
767
|
-
|
|
768
|
-
**Decision**: Hono as the underlying web framework
|
|
769
|
-
|
|
770
|
-
**Reasons**:
|
|
771
|
-
- **TypeScript First**: Better type inference
|
|
772
|
-
- **Performance**: Faster than Express
|
|
773
|
-
- **Modern**: Built for Edge/Serverless
|
|
774
|
-
- **Lightweight**: Smaller bundle size
|
|
775
|
-
|
|
776
|
-
---
|
|
777
|
-
|
|
778
|
-
### 6. Why TypeBox over Zod?
|
|
779
|
-
|
|
780
|
-
**Decision**: TypeBox for schema validation
|
|
781
|
-
|
|
782
|
-
**Reasons**:
|
|
783
|
-
- **JSON Schema**: Standard, interoperable
|
|
784
|
-
- **Performance**: Faster validation (JIT compilation)
|
|
785
|
-
- **OpenAPI**: Easy OpenAPI generation
|
|
786
|
-
- **Smaller**: Smaller bundle size
|
|
787
|
-
|
|
788
|
-
**Note**: Both are supported, TypeBox is the default
|
|
789
|
-
|
|
790
|
-
---
|
|
791
|
-
|
|
792
|
-
## Extension Points
|
|
793
|
-
|
|
794
|
-
### 1. Custom Middleware
|
|
795
|
-
|
|
796
|
-
Add global or route-specific middleware:
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
// server.config.ts
|
|
800
|
-
import { defineServerConfig } from '@spfn/core/server';
|
|
801
|
-
import { defineMiddleware } from '@spfn/core/route';
|
|
802
|
-
|
|
803
|
-
const rateLimitMiddleware = defineMiddleware('rateLimit', async (c, next) => {
|
|
804
|
-
const ip = c.req.header('x-forwarded-for');
|
|
805
|
-
if (await isRateLimited(ip)) {
|
|
806
|
-
return c.json({ error: 'Rate limit exceeded' }, 429);
|
|
807
|
-
}
|
|
808
|
-
await next();
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
export default defineServerConfig()
|
|
812
|
-
.middlewares([rateLimitMiddleware])
|
|
813
|
-
.build();
|
|
814
|
-
|
|
815
|
-
// Route can skip middleware
|
|
816
|
-
export const publicRoute = route.get('/public')
|
|
817
|
-
.middleware({ skip: ['rateLimit'] })
|
|
818
|
-
.handler(async (c) => { ... });
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
---
|
|
822
|
-
|
|
823
|
-
### 2. Custom Interceptors
|
|
824
|
-
|
|
825
|
-
Add request/response interceptors:
|
|
826
|
-
|
|
827
|
-
```typescript
|
|
828
|
-
// app/api/rpc/[routeName]/route.ts
|
|
829
|
-
import { appRouter } from '@/server/router';
|
|
830
|
-
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
127
|
+
getUser: route.get('/users/:id')
|
|
128
|
+
.input({ params: Type.Object({ id: Type.String() }) })
|
|
129
|
+
.handler(async (c) =>
|
|
130
|
+
{
|
|
131
|
+
const { params } = await c.data(); // params.id: string
|
|
132
|
+
return { id: params.id, name: 'John' }; // return type inferred
|
|
133
|
+
}),
|
|
831
134
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
135
|
+
createUser: route.post('/users')
|
|
136
|
+
.input({ body: Type.Object({ name: Type.String() }) })
|
|
137
|
+
.use([Transactional()]) // auto commit/rollback
|
|
138
|
+
.handler(async (c) =>
|
|
835
139
|
{
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const isAdmin = await checkAdminRole(ctx.cookies);
|
|
840
|
-
if (!isAdmin) {
|
|
841
|
-
throw new Error('Unauthorized');
|
|
842
|
-
}
|
|
843
|
-
await next();
|
|
844
|
-
},
|
|
845
|
-
},
|
|
846
|
-
],
|
|
140
|
+
const { body } = await c.data();
|
|
141
|
+
return { id: '2', name: body.name };
|
|
142
|
+
}),
|
|
847
143
|
});
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
---
|
|
851
|
-
|
|
852
|
-
### 3. Plugin System
|
|
853
|
-
|
|
854
|
-
Create SPFN packages with auto-discovery:
|
|
855
|
-
|
|
856
|
-
```typescript
|
|
857
|
-
// @yourcompany/spfn-analytics package
|
|
858
|
-
import { registerInterceptors } from '@spfn/core/nextjs/server';
|
|
859
|
-
|
|
860
|
-
registerInterceptors('analytics', [
|
|
861
|
-
{
|
|
862
|
-
pathPattern: '*',
|
|
863
|
-
response: async (ctx, next) => {
|
|
864
|
-
await analytics.track({
|
|
865
|
-
path: ctx.path,
|
|
866
|
-
status: ctx.response.status,
|
|
867
|
-
duration: Date.now() - ctx.metadata.startTime,
|
|
868
|
-
});
|
|
869
|
-
await next();
|
|
870
|
-
},
|
|
871
|
-
},
|
|
872
|
-
]);
|
|
873
|
-
|
|
874
|
-
// App: Auto-discovery
|
|
875
|
-
import '@yourcompany/spfn-analytics/adapters/nextjs';
|
|
876
|
-
export { GET, POST } from '@spfn/core/nextjs/server';
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
---
|
|
880
|
-
|
|
881
|
-
### 4. Custom Database Helpers
|
|
882
|
-
|
|
883
|
-
Extend database layer with domain-specific functions:
|
|
884
|
-
|
|
885
|
-
```typescript
|
|
886
|
-
// src/server/repositories/users.repository.ts
|
|
887
|
-
import { findOne, create } from '@spfn/core/db';
|
|
888
|
-
import { users } from '../entities/users';
|
|
889
|
-
|
|
890
|
-
export async function findUserByEmail(email: string) {
|
|
891
|
-
return findOne(users, { email });
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
export async function createUserWithProfile(data) {
|
|
895
|
-
const user = await create(users, data.user);
|
|
896
|
-
const profile = await create(profiles, {
|
|
897
|
-
...data.profile,
|
|
898
|
-
userId: user.id,
|
|
899
|
-
});
|
|
900
|
-
return { user, profile };
|
|
901
|
-
}
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
---
|
|
905
|
-
|
|
906
|
-
## Migration Guides
|
|
907
|
-
|
|
908
|
-
### From Contract-Based to define-route
|
|
909
|
-
|
|
910
|
-
**Before** (Deprecated):
|
|
911
|
-
```typescript
|
|
912
|
-
// contract.ts
|
|
913
|
-
export const getUserContract = {
|
|
914
|
-
method: 'GET' as const,
|
|
915
|
-
path: '/users/:id',
|
|
916
|
-
params: Type.Object({ id: Type.String() }),
|
|
917
|
-
response: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
918
|
-
} as const satisfies RouteContract;
|
|
919
|
-
|
|
920
|
-
// route.ts
|
|
921
|
-
import { createApp } from '@spfn/core/route';
|
|
922
|
-
const app = createApp();
|
|
923
|
-
app.bind(getUserContract, async (c) => { ... });
|
|
924
|
-
export default app;
|
|
925
|
-
```
|
|
926
144
|
|
|
927
|
-
**After** (Current):
|
|
928
|
-
```typescript
|
|
929
|
-
// routes/users.ts
|
|
930
|
-
import { route } from '@spfn/core/route';
|
|
931
|
-
|
|
932
|
-
export const getUser = route.get('/users/:id')
|
|
933
|
-
.input({
|
|
934
|
-
params: Type.Object({ id: Type.String() }),
|
|
935
|
-
})
|
|
936
|
-
.handler(async (c) => {
|
|
937
|
-
const { params } = await c.data();
|
|
938
|
-
return { id: params.id, name: 'John' };
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// router.ts
|
|
942
|
-
import { defineRouter } from '@spfn/core/route';
|
|
943
|
-
export const appRouter = defineRouter({ getUser });
|
|
944
145
|
export type AppRouter = typeof appRouter;
|
|
945
146
|
```
|
|
946
147
|
|
|
947
|
-
**Benefits**:
|
|
948
|
-
- Better type inference
|
|
949
|
-
- Explicit imports (tree-shaking)
|
|
950
|
-
- No separate contract files
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
### From File-Based to define-route
|
|
955
|
-
|
|
956
|
-
**Before** (Deprecated):
|
|
957
148
|
```typescript
|
|
958
|
-
//
|
|
959
|
-
|
|
149
|
+
// server/index.ts — ③
|
|
150
|
+
import { defineServerConfig, startServer } from '@spfn/core/server';
|
|
151
|
+
import { appRouter } from './router';
|
|
960
152
|
|
|
961
|
-
|
|
153
|
+
export default defineServerConfig().port(8790).routes(appRouter).build();
|
|
154
|
+
await startServer(); // Hono on :8790
|
|
962
155
|
```
|
|
963
156
|
|
|
964
|
-
**After** (Current):
|
|
965
157
|
```typescript
|
|
966
|
-
//
|
|
967
|
-
export const getUser = route.get('/users/:id')...
|
|
968
|
-
|
|
969
|
-
// router.ts
|
|
970
|
-
export const appRouter = defineRouter({ getUser });
|
|
971
|
-
|
|
972
|
-
// server.config.ts
|
|
973
|
-
export default defineServerConfig()
|
|
974
|
-
.routes(appRouter)
|
|
975
|
-
.build();
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
**Benefits**:
|
|
979
|
-
- Explicit route registration
|
|
980
|
-
- Better IDE support
|
|
981
|
-
- No runtime file system scanning
|
|
982
|
-
|
|
983
|
-
---
|
|
984
|
-
|
|
985
|
-
### From ContractClient to tRPC-Style API
|
|
986
|
-
|
|
987
|
-
**Before** (Old Client):
|
|
988
|
-
```typescript
|
|
989
|
-
import { ContractClient } from '@spfn/core/client';
|
|
990
|
-
import { getUserContract } from '@/contracts/users';
|
|
991
|
-
|
|
992
|
-
const client = new ContractClient({ baseUrl: 'http://localhost:8790' });
|
|
993
|
-
const user = await client.call(getUserContract, {
|
|
994
|
-
params: { id: '123' },
|
|
995
|
-
});
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
**After** (Current):
|
|
999
|
-
```typescript
|
|
1000
|
-
import { api } from '@spfn/core/nextjs/server';
|
|
1001
|
-
|
|
1002
|
-
const user = await api.getUser
|
|
1003
|
-
.params({ id: '123' })
|
|
1004
|
-
.call();
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
**Benefits**:
|
|
1008
|
-
- tRPC-style DX
|
|
1009
|
-
- Method chaining
|
|
1010
|
-
- Better type inference
|
|
1011
|
-
- Automatic cookie handling
|
|
1012
|
-
|
|
1013
|
-
---
|
|
1014
|
-
|
|
1015
|
-
## Module Exports
|
|
1016
|
-
|
|
1017
|
-
### Main Server Exports
|
|
1018
|
-
|
|
1019
|
-
```typescript
|
|
1020
|
-
import {
|
|
1021
|
-
startServer,
|
|
1022
|
-
createServer,
|
|
1023
|
-
defineServerConfig,
|
|
1024
|
-
} from '@spfn/core';
|
|
1025
|
-
```
|
|
1026
|
-
|
|
1027
|
-
### Route System
|
|
1028
|
-
|
|
1029
|
-
```typescript
|
|
1030
|
-
import {
|
|
1031
|
-
route,
|
|
1032
|
-
defineRouter,
|
|
1033
|
-
} from '@spfn/core/route';
|
|
1034
|
-
|
|
1035
|
-
import type {
|
|
1036
|
-
RouteDef,
|
|
1037
|
-
Router,
|
|
1038
|
-
RouteInput,
|
|
1039
|
-
} from '@spfn/core/route';
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
### Database System
|
|
1043
|
-
|
|
1044
|
-
```typescript
|
|
1045
|
-
import {
|
|
1046
|
-
getDatabase,
|
|
1047
|
-
findOne,
|
|
1048
|
-
findMany,
|
|
1049
|
-
create,
|
|
1050
|
-
createMany,
|
|
1051
|
-
updateOne,
|
|
1052
|
-
updateMany,
|
|
1053
|
-
deleteOne,
|
|
1054
|
-
deleteMany,
|
|
1055
|
-
count,
|
|
1056
|
-
} from '@spfn/core/db';
|
|
1057
|
-
|
|
1058
|
-
import {
|
|
1059
|
-
Transactional,
|
|
1060
|
-
getTransaction,
|
|
1061
|
-
runWithTransaction,
|
|
1062
|
-
} from '@spfn/core/db';
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
### Client System
|
|
1066
|
-
|
|
1067
|
-
```typescript
|
|
1068
|
-
// Client-safe exports (works in Client Components)
|
|
1069
|
-
import {
|
|
1070
|
-
createApi,
|
|
1071
|
-
ApiError,
|
|
1072
|
-
} from '@spfn/core/nextjs';
|
|
1073
|
-
|
|
1074
|
-
// Server-only exports (API Routes)
|
|
1075
|
-
import {
|
|
1076
|
-
createRpcProxy,
|
|
1077
|
-
registerInterceptors,
|
|
1078
|
-
} from '@spfn/core/nextjs/server';
|
|
1079
|
-
```
|
|
1080
|
-
|
|
1081
|
-
### Error System
|
|
1082
|
-
|
|
1083
|
-
```typescript
|
|
1084
|
-
import {
|
|
1085
|
-
ApiError,
|
|
1086
|
-
ValidationError,
|
|
1087
|
-
NotFoundError,
|
|
1088
|
-
ConflictError,
|
|
1089
|
-
UnauthorizedError,
|
|
1090
|
-
} from '@spfn/core/errors';
|
|
1091
|
-
```
|
|
1092
|
-
|
|
1093
|
-
### Logger System
|
|
1094
|
-
|
|
1095
|
-
```typescript
|
|
1096
|
-
import { logger } from '@spfn/core';
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
### Cache System
|
|
1100
|
-
|
|
1101
|
-
```typescript
|
|
1102
|
-
import {
|
|
1103
|
-
initCache,
|
|
1104
|
-
getCache,
|
|
1105
|
-
getCacheRead,
|
|
1106
|
-
isCacheDisabled,
|
|
1107
|
-
closeCache,
|
|
1108
|
-
} from '@spfn/core/cache';
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
### Environment System
|
|
1112
|
-
|
|
1113
|
-
```typescript
|
|
1114
|
-
import {
|
|
1115
|
-
defineEnvSchema,
|
|
1116
|
-
createEnvRegistry,
|
|
1117
|
-
envString,
|
|
1118
|
-
envNumber,
|
|
1119
|
-
envBoolean,
|
|
1120
|
-
envEnum,
|
|
1121
|
-
} from '@spfn/core/env';
|
|
1122
|
-
```
|
|
1123
|
-
|
|
1124
|
-
### Configuration System
|
|
1125
|
-
|
|
1126
|
-
```typescript
|
|
1127
|
-
import { env, envSchema } from '@spfn/core/config';
|
|
1128
|
-
```
|
|
1129
|
-
|
|
1130
|
-
### Job System
|
|
1131
|
-
|
|
1132
|
-
```typescript
|
|
1133
|
-
import {
|
|
1134
|
-
job,
|
|
1135
|
-
defineJobRouter,
|
|
1136
|
-
registerJobs,
|
|
1137
|
-
initBoss,
|
|
1138
|
-
getBoss,
|
|
1139
|
-
stopBoss,
|
|
1140
|
-
} from '@spfn/core/job';
|
|
1141
|
-
|
|
1142
|
-
import type {
|
|
1143
|
-
JobDef,
|
|
1144
|
-
JobRouter,
|
|
1145
|
-
JobOptions,
|
|
1146
|
-
BossOptions,
|
|
1147
|
-
InferJobInput,
|
|
1148
|
-
} from '@spfn/core/job';
|
|
1149
|
-
```
|
|
1150
|
-
|
|
1151
|
-
### Event System
|
|
1152
|
-
|
|
1153
|
-
```typescript
|
|
1154
|
-
import { defineEvent } from '@spfn/core/event';
|
|
1155
|
-
|
|
1156
|
-
import type {
|
|
1157
|
-
EventDef,
|
|
1158
|
-
EventHandler,
|
|
1159
|
-
InferEventPayload,
|
|
1160
|
-
PubSubCache,
|
|
1161
|
-
} from '@spfn/core/event';
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
---
|
|
1165
|
-
|
|
1166
|
-
## Quick Reference
|
|
1167
|
-
|
|
1168
|
-
### Environment Variables
|
|
1169
|
-
|
|
1170
|
-
```bash
|
|
1171
|
-
# Database (required)
|
|
1172
|
-
DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
|
1173
|
-
|
|
1174
|
-
# Database Read Replica (optional)
|
|
1175
|
-
DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
|
|
1176
|
-
|
|
1177
|
-
# Cache - Valkey/Redis (optional)
|
|
1178
|
-
CACHE_URL=redis://localhost:6379
|
|
1179
|
-
CACHE_WRITE_URL=redis://master:6379
|
|
1180
|
-
CACHE_READ_URL=redis://replica:6379
|
|
1181
|
-
|
|
1182
|
-
# Next.js App URL (for Server Components calling API Routes)
|
|
1183
|
-
SPFN_APP_URL=http://localhost:3000
|
|
1184
|
-
|
|
1185
|
-
# SPFN API Server URL (for API Route Proxy)
|
|
1186
|
-
SPFN_API_URL=http://localhost:8790
|
|
1187
|
-
|
|
1188
|
-
# Server
|
|
1189
|
-
PORT=8790
|
|
1190
|
-
HOST=localhost
|
|
1191
|
-
NODE_ENV=development
|
|
1192
|
-
|
|
1193
|
-
# Server Timeouts (optional, milliseconds)
|
|
1194
|
-
SERVER_TIMEOUT=120000
|
|
1195
|
-
SERVER_KEEPALIVE_TIMEOUT=65000
|
|
1196
|
-
SERVER_HEADERS_TIMEOUT=60000
|
|
1197
|
-
SHUTDOWN_TIMEOUT=30000
|
|
1198
|
-
|
|
1199
|
-
# Logger (optional)
|
|
1200
|
-
LOGGER_ADAPTER=pino
|
|
1201
|
-
LOGGER_FILE_ENABLED=true
|
|
1202
|
-
LOG_DIR=/var/log/myapp
|
|
1203
|
-
```
|
|
1204
|
-
|
|
1205
|
-
---
|
|
1206
|
-
|
|
1207
|
-
### Basic Setup
|
|
1208
|
-
|
|
1209
|
-
**1. Install**
|
|
1210
|
-
```bash
|
|
1211
|
-
npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
**2. Define Routes**
|
|
1215
|
-
```typescript
|
|
1216
|
-
// src/server/routes/users.ts
|
|
1217
|
-
export const getUser = route.get('/users/:id')
|
|
1218
|
-
.input({ params: Type.Object({ id: Type.String() }) })
|
|
1219
|
-
.handler(async (c) => {
|
|
1220
|
-
const { params } = await c.data();
|
|
1221
|
-
return { id: params.id, name: 'John' };
|
|
1222
|
-
});
|
|
1223
|
-
```
|
|
1224
|
-
|
|
1225
|
-
**3. Create Router**
|
|
1226
|
-
```typescript
|
|
1227
|
-
// src/server/router.ts
|
|
1228
|
-
export const appRouter = defineRouter({ getUser });
|
|
1229
|
-
export type AppRouter = typeof appRouter;
|
|
1230
|
-
```
|
|
1231
|
-
|
|
1232
|
-
**4. Configure Server**
|
|
1233
|
-
```typescript
|
|
1234
|
-
// src/server/server.config.ts
|
|
1235
|
-
export default defineServerConfig()
|
|
1236
|
-
.port(8790)
|
|
1237
|
-
.routes(appRouter)
|
|
1238
|
-
.build();
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
**5. Create RPC Proxy (Next.js)**
|
|
1242
|
-
```typescript
|
|
1243
|
-
// app/api/rpc/[routeName]/route.ts
|
|
1244
|
-
import { appRouter } from '@/server/router';
|
|
158
|
+
// app/api/rpc/[routeName]/route.ts — ⑤ (server-only)
|
|
1245
159
|
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
160
|
+
import { routeMap } from '@/generated/route-map'; // ④ codegen output
|
|
1246
161
|
|
|
1247
|
-
export const { GET, POST } = createRpcProxy({
|
|
162
|
+
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
1248
163
|
```
|
|
1249
164
|
|
|
1250
|
-
**6. Use Client**
|
|
1251
165
|
```typescript
|
|
1252
|
-
//
|
|
166
|
+
// lib/api.ts — ⑥ (client-safe; no codegen)
|
|
1253
167
|
import { createApi } from '@spfn/core/nextjs';
|
|
1254
168
|
import type { AppRouter } from '@/server/router';
|
|
1255
169
|
|
|
1256
|
-
const api = createApi<AppRouter>();
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
-
|
|
1266
|
-
|
|
1267
|
-
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
-
|
|
1272
|
-
|
|
1273
|
-
-
|
|
1274
|
-
|
|
1275
|
-
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
npm test -- --coverage # With coverage
|
|
1296
|
-
```
|
|
1297
|
-
|
|
1298
|
-
**Test Coverage**: 650+ tests across all modules
|
|
1299
|
-
|
|
1300
|
-
---
|
|
1301
|
-
|
|
1302
|
-
## License
|
|
1303
|
-
|
|
1304
|
-
MIT
|
|
1305
|
-
|
|
1306
|
-
---
|
|
1307
|
-
|
|
1308
|
-
Part of the [Superfunction Framework](https://github.com/spfn/spfn)
|
|
170
|
+
export const api = createApi<AppRouter>();
|
|
171
|
+
|
|
172
|
+
// anywhere (RSC / client / server action):
|
|
173
|
+
const user = await api.getUser.call({ params: { id: '123' } }); // typed { id, name }
|
|
174
|
+
const made = await api.createUser.call({ body: { name: 'A' } });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Pitfalls
|
|
178
|
+
|
|
179
|
+
- **No root barrel.** `import … from '@spfn/core'` does not resolve. Import from a subpath
|
|
180
|
+
(`@spfn/core/route`, `@spfn/core/db`, …). The list above *is* the complete public surface.
|
|
181
|
+
- **Use `@spfn/core/nextjs` for the client, not `@spfn/core/client`.** `package.json` still lists a
|
|
182
|
+
`./client` export, but the build does not emit it (no `src/client`; the tsup `client` entry is
|
|
183
|
+
disabled). `createApi` / `ApiError` and all client types ship from **`@spfn/core/nextjs`**. Treat
|
|
184
|
+
`@spfn/core/client` as non-functional.
|
|
185
|
+
- **Client vs. server boundary is load-bearing.** `@spfn/core/nextjs/server` pulls in
|
|
186
|
+
`next/headers` + `next/server`; never import it from a Client Component. `@spfn/core/env/loader`,
|
|
187
|
+
`@spfn/core/event/sse`, `@spfn/core/event/ws` are server-only (`node:fs` / Hono / `node:crypto`).
|
|
188
|
+
Client code uses the `*/client` and isomorphic entry points only.
|
|
189
|
+
- **`db/manager`, `db/schema`, `db/transaction` are not package subpaths.** Importing
|
|
190
|
+
`@spfn/core/db/transaction` fails to resolve — import those symbols from `@spfn/core/db`.
|
|
191
|
+
- **The client needs no codegen; the proxy does.** `createApi<AppRouter>()` is metadata-free and
|
|
192
|
+
driven by the `AppRouter` *type*. The generated `routeMap` is consumed by `createRpcProxy`. A
|
|
193
|
+
`routeName` missing from the merged `routeMap` is a **proxy 404**, not a backend 404 — re-run
|
|
194
|
+
codegen after adding routes.
|
|
195
|
+
- **The proxy decides the real HTTP method.** The client only sends GET/POST to `/api/rpc/...`; a
|
|
196
|
+
PUT/PATCH/DELETE route still works because the backend method comes from `routeMap[routeName]`.
|
|
197
|
+
- **Cache/event/job degrade or depend on optional deps.** `@spfn/core/cache` runs disabled (getters
|
|
198
|
+
return `undefined`) without `CACHE_*` config or `ioredis`; WebSocket events need the optional `ws`
|
|
199
|
+
dep. Don't assume they throw.
|
|
200
|
+
|
|
201
|
+
## Related packages
|
|
202
|
+
|
|
203
|
+
Other SPFN packages build on `@spfn/core`:
|
|
204
|
+
|
|
205
|
+
- [`@spfn/auth`](../auth/README.md) — auth/session; exports `authRouteMap` and auto-registers proxy
|
|
206
|
+
interceptors (merge its route map into `createRpcProxy`).
|
|
207
|
+
- `@spfn/cms`, `@spfn/workflow`, `@spfn/notification`, `@spfn/monitor`, `@spfn/cli`
|
|
208
|
+
— see each package's README under `packages/`.
|