@spfn/core 0.2.0-beta.1 → 0.2.0-beta.10

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.
Files changed (64) hide show
  1. package/README.md +262 -1092
  2. package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +159 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +36 -0
  7. package/dist/config/index.js +15 -6
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +13 -0
  10. package/dist/db/index.js +40 -6
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +82 -3
  13. package/dist/env/index.js +81 -14
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +87 -0
  16. package/dist/env/loader.js +70 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +3 -70
  19. package/dist/event/index.js +10 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +82 -0
  22. package/dist/event/sse/client.js +115 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +40 -0
  25. package/dist/event/sse/index.js +92 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +54 -8
  28. package/dist/job/index.js +61 -12
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +102 -11
  31. package/dist/middleware/index.js +2 -2
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/nextjs/index.d.ts +2 -2
  34. package/dist/nextjs/index.js +36 -4
  35. package/dist/nextjs/index.js.map +1 -1
  36. package/dist/nextjs/server.d.ts +62 -15
  37. package/dist/nextjs/server.js +102 -33
  38. package/dist/nextjs/server.js.map +1 -1
  39. package/dist/route/index.d.ts +227 -15
  40. package/dist/route/index.js +307 -31
  41. package/dist/route/index.js.map +1 -1
  42. package/dist/route/types.d.ts +2 -31
  43. package/dist/router-Di7ENoah.d.ts +151 -0
  44. package/dist/server/index.d.ts +153 -6
  45. package/dist/server/index.js +216 -14
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/types-B-e_f2dQ.d.ts +121 -0
  48. package/dist/{types-DRG2XMTR.d.ts → types-BOPTApC2.d.ts} +91 -3
  49. package/docs/cache.md +133 -0
  50. package/docs/codegen.md +74 -0
  51. package/docs/database.md +346 -0
  52. package/docs/entity.md +539 -0
  53. package/docs/env.md +477 -0
  54. package/docs/errors.md +319 -0
  55. package/docs/event.md +116 -0
  56. package/docs/file-upload.md +717 -0
  57. package/docs/job.md +131 -0
  58. package/docs/logger.md +108 -0
  59. package/docs/middleware.md +337 -0
  60. package/docs/nextjs.md +241 -0
  61. package/docs/repository.md +496 -0
  62. package/docs/route.md +497 -0
  63. package/docs/server.md +307 -0
  64. package/package.json +18 -3
package/README.md CHANGED
@@ -1,1223 +1,393 @@
1
- # @spfn/core - Technical Architecture Documentation
1
+ # @spfn/core
2
2
 
3
- Full-stack type-safe framework for building Next.js + Node.js applications with end-to-end type inference.
3
+ Type-safe Node.js backend framework built on Hono + Drizzle ORM.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/@spfn%2Fcore.svg)](https://www.npmjs.com/package/@spfn/core)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
8
8
 
9
- > **Alpha Release**: SPFN is currently in alpha. APIs may change. Use `@alpha` tag for installation.
9
+ > **Beta Release**: Core APIs are stable but may have minor changes before 1.0.
10
10
 
11
- ---
12
-
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**.
11
+ ## Installation
31
12
 
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[] }
13
+ ```bash
14
+ pnpm add @spfn/core
51
15
  ```
52
16
 
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
17
+ ## Quick Start
63
18
 
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
- +---------------------------------------------------------------+
142
- ```
143
-
144
- ### Request Flow Example
19
+ ### 1. Define Entity
145
20
 
146
21
  ```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: c.success() / c.error()
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**:
22
+ // src/server/entities/users.ts
23
+ import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
24
+ import { id, timestamps } from '@spfn/core/db';
311
25
 
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);
26
+ export const users = pgTable('users', {
27
+ id: id(),
28
+ email: text('email').notNull().unique(),
29
+ name: text('name').notNull(),
30
+ isActive: boolean('is_active').notNull().default(true),
31
+ ...timestamps()
322
32
  });
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
33
 
34
+ export type User = typeof users.$inferSelect;
35
+ export type NewUser = typeof users.$inferInsert;
333
36
  ```
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
37
 
400
- **[→ Full Documentation](./src/logger/README.md)**
38
+ ### 2. Create Repository
401
39
 
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/`)
40
+ ```typescript
41
+ // src/server/repositories/user.repository.ts
42
+ import { BaseRepository } from '@spfn/core/db';
43
+ import { users, type User, type NewUser } from '../entities/users';
421
44
 
422
- **Purpose**: Named middleware with skip control
45
+ export class UserRepository extends BaseRepository
46
+ {
47
+ async findById(id: string): Promise<User | null>
48
+ {
49
+ return this._findOne(users, { id });
50
+ }
423
51
 
424
- **Key Components**:
52
+ async findByEmail(email: string): Promise<User | null>
53
+ {
54
+ return this._findOne(users, { email });
55
+ }
425
56
 
426
- - `defineMiddleware()`: Create named middleware
427
- - Skip control: Routes can skip specific middlewares
428
- - Built-in: Logger, CORS, Error handler
57
+ async findAll(): Promise<User[]>
58
+ {
59
+ return this._findMany(users);
60
+ }
429
61
 
430
- **[→ Full Documentation](./src/middleware/README.md)**
62
+ async create(data: NewUser): Promise<User>
63
+ {
64
+ return this._create(users, data);
65
+ }
431
66
 
432
- ---
67
+ async update(id: string, data: Partial<NewUser>): Promise<User | null>
68
+ {
69
+ return this._updateOne(users, { id }, data);
70
+ }
433
71
 
434
- ## Type System
72
+ async delete(id: string): Promise<User | null>
73
+ {
74
+ return this._deleteOne(users, { id });
75
+ }
76
+ }
77
+ ```
435
78
 
436
- ### End-to-End Type Safety Flow
79
+ ### 3. Define Routes
437
80
 
438
81
  ```typescript
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
82
  // src/server/routes/users.ts
453
83
  import { route } from '@spfn/core/route';
84
+ import { Transactional } from '@spfn/core/db';
454
85
  import { Type } from '@sinclair/typebox';
86
+ import { UserRepository } from '../repositories/user.repository';
87
+
88
+ const userRepo = new UserRepository();
89
+
90
+ export const getUsers = route.get('/users')
91
+ .handler(async () => {
92
+ return userRepo.findAll();
93
+ });
455
94
 
456
95
  export const getUser = route.get('/users/:id')
457
96
  .input({
458
- params: Type.Object({
459
- id: Type.String(),
460
- }),
461
- query: Type.Object({
462
- include: Type.Optional(Type.String()),
463
- }),
97
+ params: Type.Object({ id: Type.String() })
464
98
  })
465
99
  .handler(async (c) => {
466
- const { params, query } = await c.data();
467
- // ^? { params: { id: string }, query: { include?: string } }
100
+ const { params } = await c.data();
101
+ const user = await userRepo.findById(params.id);
468
102
 
469
- const user = await findOne(users, { id: params.id });
470
- // ^? User | null
103
+ if (!user)
104
+ {
105
+ throw new Error('User not found');
106
+ }
471
107
 
472
- return c.success(user);
473
- // ^? Response inferred from return value
108
+ return user;
474
109
  });
475
110
 
476
- // 3. Router Type Export
477
- // src/server/router.ts
478
- export const appRouter = defineRouter({
479
- getUser,
480
- // ... other routes
481
- });
111
+ export const createUser = route.post('/users')
112
+ .input({
113
+ body: Type.Object({
114
+ email: Type.String({ format: 'email' }),
115
+ name: Type.String({ minLength: 1 })
116
+ })
117
+ })
118
+ .use([Transactional()])
119
+ .handler(async (c) => {
120
+ const { body } = await c.data();
121
+ return userRepo.create(body);
122
+ });
482
123
 
483
- export type AppRouter = typeof appRouter;
484
- // ^? Router<{ getUser: RouteDef<..., ...>, ... }>
124
+ export const updateUser = route.patch('/users/:id')
125
+ .input({
126
+ params: Type.Object({ id: Type.String() }),
127
+ body: Type.Object({
128
+ email: Type.Optional(Type.String({ format: 'email' })),
129
+ name: Type.Optional(Type.String({ minLength: 1 }))
130
+ })
131
+ })
132
+ .use([Transactional()])
133
+ .handler(async (c) => {
134
+ const { params, body } = await c.data();
135
+ const user = await userRepo.update(params.id, body);
485
136
 
486
- // 4. Client API Call (Next.js)
487
- // app/users/[id]/page.tsx
488
- import { api } from '@spfn/core/nextjs/server';
137
+ if (!user)
138
+ {
139
+ throw new Error('User not found');
140
+ }
489
141
 
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)
142
+ return user;
143
+ });
495
144
 
496
- // Full type inference chain:
497
- // User type → RouteDef → Router → Client API → return type
498
- ```
145
+ export const deleteUser = route.delete('/users/:id')
146
+ .input({
147
+ params: Type.Object({ id: Type.String() })
148
+ })
149
+ .use([Transactional()])
150
+ .handler(async (c) => {
151
+ const { params } = await c.data();
152
+ const user = await userRepo.delete(params.id);
499
153
 
500
- ### Type Inference Utilities
154
+ if (!user)
155
+ {
156
+ throw new Error('User not found');
157
+ }
501
158
 
502
- ```typescript
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
- };
159
+ return { success: true };
160
+ });
517
161
  ```
518
162
 
519
- ---
520
-
521
- ## Integration Points
522
-
523
- ### 1. Server ↔ Database: Transaction Context
524
-
525
- **Mechanism**: AsyncLocalStorage propagates transaction across async calls
163
+ ### 4. Configure Server
526
164
 
527
165
  ```typescript
528
- // Route handler
529
- app.bind(createPostContract, [Transactional()], async (c) => {
530
- // AsyncLocalStorage stores transaction
531
- const post = await create(posts, { title: 'Hello' });
532
-
533
- // Same transaction is used automatically
534
- const tag = await create(tags, { postId: post.id, name: 'news' });
166
+ // src/server/server.config.ts
167
+ import { defineServerConfig, defineRouter } from '@spfn/core/server';
168
+ import * as userRoutes from './routes/users';
535
169
 
536
- // Auto-commit on success, auto-rollback on error
537
- return c.json(post);
170
+ const appRouter = defineRouter({
171
+ ...userRoutes
538
172
  });
539
- ```
540
-
541
- **Why**: No need to pass transaction object through function calls
542
173
 
543
- ---
544
-
545
- ### 2. Server ↔ Client: Type Inference
546
-
547
- **Mechanism**: `typeof appRouter` captures all route types
174
+ export default defineServerConfig()
175
+ .port(8790)
176
+ .routes(appRouter)
177
+ .build();
548
178
 
549
- ```typescript
550
- // Server: Export router type
551
- export const appRouter = defineRouter({ getUser, createUser });
552
179
  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
561
- ```
562
-
563
- **Why**: Single source of truth (server) → client types automatically sync
564
-
565
- ---
566
-
567
- ### 3. Next.js ↔ SPFN: RPC Proxy
568
-
569
- **Mechanism**: Next.js API Route resolves routeName and forwards to SPFN server
570
-
571
- ```typescript
572
- // app/api/rpc/[routeName]/route.ts
573
- import { appRouter } from '@/server/router';
574
- import { createRpcProxy } from '@spfn/core/nextjs/server';
575
-
576
- export const { GET, POST } = createRpcProxy({ router: appRouter });
577
-
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
180
  ```
584
181
 
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
590
-
591
- ---
592
-
593
- ### 4. Packages: Registry System
594
-
595
- **Mechanism**: Packages register interceptors on import
182
+ ### 5. Start Server
596
183
 
597
184
  ```typescript
598
- // @spfn/auth package
599
- import { registerInterceptors } from '@spfn/core/nextjs/server';
185
+ // src/server/index.ts
186
+ import { startServer } from '@spfn/core/server';
600
187
 
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
188
+ await startServer();
615
189
  ```
616
190
 
617
- **Why**: Zero-config integration for plugin packages
191
+ ### 6. Environment Variables
618
192
 
619
- ---
620
-
621
- ## Design Decisions
622
-
623
- ### 1. Why tRPC-Style API over REST Client?
624
-
625
- **Decision**: Method chaining with `.params().query().call()`
626
-
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
632
-
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
- });
193
+ ```bash
194
+ # .env
195
+ DATABASE_URL=postgresql://localhost:5432/mydb
196
+ PORT=8790
645
197
  ```
646
198
 
647
199
  ---
648
200
 
649
- ### 2. Why API Route Proxy Pattern?
650
-
651
- **Decision**: Next.js API Route forwards to SPFN server
652
-
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
658
-
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
663
-
664
- ---
665
-
666
- ### 3. Why define-route over File-Based Routing?
667
-
668
- **Decision**: Explicit route imports with `defineRouter()`
669
-
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
- ---
685
-
686
- ### 4. Why AsyncLocalStorage for Transactions?
687
-
688
- **Decision**: Transaction propagation without explicit passing
201
+ ## Architecture Overview
689
202
 
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
710
- }
711
203
  ```
712
-
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
-
747
- ```typescript
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();
763
-
764
- // Route can skip middleware
765
- export const publicRoute = route.get('/public')
766
- .middleware({ skip: ['rateLimit'] })
767
- .handler(async (c) => { ... });
204
+ ┌─────────────────────────────────────────┐
205
+ │ Routes Layer │ API endpoints + validation
206
+ │ route.get('/users/:id').handler(...) │
207
+ └──────────────┬──────────────────────────┘
208
+
209
+ ┌──────────────▼──────────────────────────┐
210
+ │ Repository Layer │ Business logic + data access
211
+ │ class UserRepository extends Base... │
212
+ └──────────────┬──────────────────────────┘
213
+
214
+ ┌──────────────▼──────────────────────────┐
215
+ │ Entity Layer │ Database schema (Drizzle)
216
+ │ pgTable('users', { id, email, ... }) │
217
+ └─────────────────────────────────────────┘
768
218
  ```
769
219
 
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
- });
797
- ```
220
+ **Key Principles:**
221
+ - **Route**: Thin layer, input validation only
222
+ - **Repository**: All business logic and DB access
223
+ - **Entity**: Schema definition only, no logic
798
224
 
799
225
  ---
800
226
 
801
- ### 3. Plugin System
802
-
803
- Create SPFN packages with auto-discovery:
227
+ ## Directory Structure
804
228
 
805
- ```typescript
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
229
  ```
827
-
828
- ---
829
-
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 });
841
- }
842
-
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
- }
230
+ src/server/
231
+ ├── entities/ # Database schema
232
+ │ ├── users.ts
233
+ │ └── index.ts
234
+ ├── repositories/ # Data access + business logic
235
+ │ ├── user.repository.ts
236
+ │ └── index.ts
237
+ ├── routes/ # API routes
238
+ │ ├── users.ts
239
+ │ └── index.ts
240
+ ├── server.config.ts # Server configuration
241
+ └── index.ts # Entry point
851
242
  ```
852
243
 
853
244
  ---
854
245
 
855
- ## Migration Guides
246
+ ## Core Concepts
856
247
 
857
- ### From Contract-Based to define-route
248
+ ### Route Definition
858
249
 
859
- **Before** (Deprecated):
860
250
  ```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
- ```
875
-
876
- **After** (Current):
877
- ```typescript
878
- // routes/users.ts
879
251
  import { route } from '@spfn/core/route';
252
+ import { Type } from '@sinclair/typebox';
880
253
 
881
- export const getUser = route.get('/users/:id')
254
+ // GET with params and query
255
+ route.get('/users/:id')
882
256
  .input({
883
257
  params: Type.Object({ id: Type.String() }),
258
+ query: Type.Object({ include: Type.Optional(Type.String()) })
884
259
  })
885
260
  .handler(async (c) => {
886
- const { params } = await c.data();
887
- return c.success({ id: params.id, name: 'John' });
261
+ const { params, query } = await c.data();
262
+ // params.id, query.include are fully typed
888
263
  });
889
264
 
890
- // router.ts
891
- import { defineRouter } from '@spfn/core/route';
892
- export const appRouter = defineRouter({ getUser });
893
- export type AppRouter = typeof appRouter;
265
+ // POST with body
266
+ route.post('/users')
267
+ .input({
268
+ body: Type.Object({
269
+ email: Type.String(),
270
+ name: Type.String()
271
+ })
272
+ })
273
+ .handler(async (c) => {
274
+ const { body } = await c.data();
275
+ // body.email, body.name are fully typed
276
+ });
894
277
  ```
895
278
 
896
- **Benefits**:
897
- - Better type inference
898
- - Explicit imports (tree-shaking)
899
- - No separate contract files
900
-
901
- ---
279
+ ### Repository Pattern
902
280
 
903
- ### From File-Based to define-route
904
-
905
- **Before** (Deprecated):
906
- ```typescript
907
- // routes/users/[id].ts
908
- export default async function handler(c) { ... }
909
-
910
- // Automatic file system scanning
911
- ```
912
-
913
- **After** (Current):
914
281
  ```typescript
915
- // routes/users.ts
916
- export const getUser = route.get('/users/:id')...
282
+ import { BaseRepository } from '@spfn/core/db';
917
283
 
918
- // router.ts
919
- export const appRouter = defineRouter({ getUser });
284
+ export class UserRepository extends BaseRepository
285
+ {
286
+ // Protected helpers available:
287
+ // _findOne, _findMany, _create, _createMany
288
+ // _updateOne, _updateMany, _deleteOne, _deleteMany
289
+ // _count, _upsert
920
290
 
921
- // server.config.ts
922
- export default defineServerConfig()
923
- .routes(appRouter)
924
- .build();
291
+ async findActive()
292
+ {
293
+ return this._findMany(users, {
294
+ where: { isActive: true },
295
+ orderBy: desc(users.createdAt),
296
+ limit: 10
297
+ });
298
+ }
299
+ }
925
300
  ```
926
301
 
927
- **Benefits**:
928
- - Explicit route registration
929
- - Better IDE support
930
- - No runtime file system scanning
931
-
932
- ---
933
-
934
- ### From ContractClient to tRPC-Style API
935
-
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
- ```
302
+ ### Transaction
946
303
 
947
- **After** (Current):
948
304
  ```typescript
949
- import { api } from '@spfn/core/nextjs/server';
305
+ import { Transactional } from '@spfn/core/db';
950
306
 
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
- ---
963
-
964
- ## Module Exports
965
-
966
- ### Main Server Exports
967
-
968
- ```typescript
969
- import {
970
- startServer,
971
- createServer,
972
- defineServerConfig,
973
- } from '@spfn/core';
974
- ```
307
+ // Middleware-based (recommended)
308
+ route.post('/users')
309
+ .use([Transactional()])
310
+ .handler(async (c) => {
311
+ // Auto commit on success
312
+ // Auto rollback on error
313
+ });
975
314
 
976
- ### Route System
315
+ // Manual control
316
+ import { runWithTransaction } from '@spfn/core/db';
977
317
 
978
- ```typescript
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';
318
+ await runWithTransaction(async () => {
319
+ await userRepo.create(userData);
320
+ await profileRepo.create(profileData);
321
+ });
989
322
  ```
990
323
 
991
- ### Database System
324
+ ### Schema Helpers
992
325
 
993
326
  ```typescript
994
327
  import {
995
- getDatabase,
996
- findOne,
997
- findMany,
998
- create,
999
- createMany,
1000
- updateOne,
1001
- updateMany,
1002
- deleteOne,
1003
- deleteMany,
1004
- count,
328
+ id, // bigserial primary key
329
+ uuid, // uuid primary key
330
+ timestamps, // createdAt, updatedAt
331
+ foreignKey, // required FK with cascade
332
+ optionalForeignKey, // nullable FK
333
+ softDelete, // deletedAt, deletedBy
334
+ enumText, // type-safe enum
335
+ typedJsonb // type-safe JSONB
1005
336
  } from '@spfn/core/db';
1006
337
 
1007
- import {
1008
- Transactional,
1009
- getTransaction,
1010
- runWithTransaction,
1011
- } from '@spfn/core/db';
1012
- ```
1013
-
1014
- ### Client System
1015
-
1016
- ```typescript
1017
- // Client-safe exports (works in Client Components)
1018
- import {
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';
1028
- ```
1029
-
1030
- ### Error System
1031
-
1032
- ```typescript
1033
- import {
1034
- ApiError,
1035
- ValidationError,
1036
- NotFoundError,
1037
- ConflictError,
1038
- UnauthorizedError,
1039
- } from '@spfn/core/errors';
1040
- ```
1041
-
1042
- ### Logger System
1043
-
1044
- ```typescript
1045
- import { logger } from '@spfn/core';
1046
- ```
1047
-
1048
- ### Cache System
1049
-
1050
- ```typescript
1051
- import {
1052
- initCache,
1053
- getCache,
1054
- getCacheRead,
1055
- isCacheDisabled,
1056
- closeCache,
1057
- } from '@spfn/core/cache';
1058
- ```
1059
-
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';
338
+ export const posts = pgTable('posts', {
339
+ id: id(),
340
+ title: text('title').notNull(),
341
+ authorId: foreignKey('author', () => users.id),
342
+ status: enumText('status', ['draft', 'published']).default('draft'),
343
+ metadata: typedJsonb<{ views: number }>('metadata'),
344
+ ...timestamps(),
345
+ ...softDelete()
346
+ });
1077
347
  ```
1078
348
 
1079
349
  ---
1080
350
 
1081
- ## Quick Reference
1082
-
1083
- ### Environment Variables
351
+ ## Module Documentation
1084
352
 
1085
- ```bash
1086
- # Database (required)
1087
- DATABASE_URL=postgresql://user:pass@localhost:5432/db
1088
-
1089
- # Database Read Replica (optional)
1090
- DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
1091
-
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
1102
-
1103
- # Server
1104
- PORT=8790
1105
- HOST=localhost
1106
- NODE_ENV=development
1107
-
1108
- # Server Timeouts (optional, milliseconds)
1109
- SERVER_TIMEOUT=120000
1110
- SERVER_KEEPALIVE_TIMEOUT=65000
1111
- SERVER_HEADERS_TIMEOUT=60000
1112
- SHUTDOWN_TIMEOUT=30000
1113
-
1114
- # Logger (optional)
1115
- LOGGER_ADAPTER=pino
1116
- LOGGER_FILE_ENABLED=true
1117
- LOG_DIR=/var/log/myapp
1118
- ```
353
+ | Module | Description |
354
+ |--------|-------------|
355
+ | [Route](./docs/route.md) | Route definition, validation, response patterns |
356
+ | [Database](./docs/database.md) | Connection, helpers, transactions |
357
+ | [Entity](./docs/entity.md) | Schema definition, column helpers |
358
+ | [Repository](./docs/repository.md) | BaseRepository, CRUD patterns |
359
+ | [Middleware](./docs/middleware.md) | Named middleware, skip control |
360
+ | [Server](./docs/server.md) | Configuration, lifecycle hooks |
361
+ | [Errors](./docs/errors.md) | Error types, handling patterns |
362
+ | [Environment](./docs/env.md) | Type-safe environment variables |
363
+ | [Codegen](./docs/codegen.md) | API client generation |
364
+ | [Cache](./docs/cache.md) | Redis caching |
365
+ | [Event](./docs/event.md) | Event system |
366
+ | [Job](./docs/job.md) | Background jobs |
367
+ | [Logger](./docs/logger.md) | Logging |
368
+ | [Next.js](./docs/nextjs.md) | Next.js integration, RPC proxy |
1119
369
 
1120
370
  ---
1121
371
 
1122
- ### Basic Setup
372
+ ## CLI Commands
1123
373
 
1124
- **1. Install**
1125
374
  ```bash
1126
- npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
1127
- ```
375
+ # Migration
376
+ npx spfn db generate # Generate migration
377
+ npx spfn db migrate # Apply migration
1128
378
 
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
- });
1138
- ```
379
+ # Development
380
+ pnpm spfn:dev # Start dev server (auto codegen)
1139
381
 
1140
- **3. Create Router**
1141
- ```typescript
1142
- // src/server/router.ts
1143
- export const appRouter = defineRouter({ getUser });
1144
- export type AppRouter = typeof appRouter;
1145
- ```
382
+ # API Client Generation
383
+ pnpm spfn codegen run
1146
384
 
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' } });
385
+ # Database Studio
386
+ pnpm drizzle-kit studio
1173
387
  ```
1174
388
 
1175
389
  ---
1176
390
 
1177
- ## Documentation
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
-
1185
- ### Guides
1186
- - [Transaction Management](./src/db/docs/transactions.md)
1187
- - [Error Handling](./src/errors/README.md)
1188
- - [Logger](./src/logger/README.md)
1189
- - [Cache](./src/cache/README.md)
1190
- - [Middleware](./src/middleware/README.md)
1191
- - [Code Generation](./src/codegen/README.md)
1192
-
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
- ---
1216
-
1217
391
  ## License
1218
392
 
1219
393
  MIT
1220
-
1221
- ---
1222
-
1223
- Part of the [Superfunction Framework](https://github.com/spfn/spfn)