@spfn/core 0.2.0-beta.5 → 0.2.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1308 +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
- > **Beta Release**: SPFN is currently in beta. Core APIs are stable but may have minor changes before 1.0.
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
17
+ ## Quick Start
59
18
 
60
- ---
61
-
62
- ## System Architecture
63
-
64
- ### High-Level Overview
65
-
66
- ```
67
- +---------------------------------------------------------------+
68
- | Next.js Application |
69
- | +----------------------------------------------------------+ |
70
- | | Frontend (React) | |
71
- | | - Server Components: SSR, ISR, Static | |
72
- | | - Client Components: Interactive UI | |
73
- | +---------------------------+------------------------------+ |
74
- | | |
75
- | | import { api } from '@spfn/...' |
76
- | | api.getUser.call({ params }) |
77
- | v |
78
- | +----------------------------------------------------------+ |
79
- | | RPC Proxy (Edge/Node.js) | |
80
- | | app/api/rpc/[routeName]/route.ts | |
81
- | | | |
82
- | | 1. Resolve routeName → method/path from router | |
83
- | | | |
84
- | | 2. Request Interceptors | |
85
- | | - Auth token injection | |
86
- | | - Cookie forwarding | |
87
- | | - Header manipulation | |
88
- | | | |
89
- | | 3. Forward to SPFN Server | |
90
- | | fetch(SPFN_API_URL + resolvedPath) | |
91
- | | | |
92
- | | 4. Response Interceptors | |
93
- | | - Set HttpOnly cookies | |
94
- | | - Transform response | |
95
- | | - Error handling | |
96
- | +---------------------------+------------------------------+ |
97
- +-------------------------------+--------------------------------+
98
- |
99
- | HTTP Request
100
- v
101
- +---------------------------------------------------------------+
102
- | SPFN API Server (Node.js) |
103
- | +----------------------------------------------------------+ |
104
- | | Hono Web Framework | |
105
- | | | |
106
- | | 12-Step Middleware Pipeline: | |
107
- | | 1. Logger | |
108
- | | 2. CORS | |
109
- | | 3. Global Middlewares | |
110
- | | 4. Route-specific Middlewares | |
111
- | | 5. Request Validation (TypeBox) | |
112
- | | 6. Route Handler | |
113
- | | 7-12. Response processing, error handling | |
114
- | +---------------------------+------------------------------+ |
115
- | | |
116
- | | define-route System |
117
- | v |
118
- | +----------------------------------------------------------+ |
119
- | | Route Handlers | |
120
- | | - Type-safe input validation | |
121
- | | - Transaction middleware | |
122
- | | - Business logic | |
123
- | +---------------------------+------------------------------+ |
124
- | | |
125
- | | Database Queries |
126
- | v |
127
- | +----------------------------------------------------------+ |
128
- | | Database Layer (Drizzle ORM) | |
129
- | | - Helper functions (findOne, create, etc.) | |
130
- | | - Transaction propagation (AsyncLocalStorage) | |
131
- | | - Read/Write separation | |
132
- | +---------------------------+------------------------------+ |
133
- +-------------------------------+--------------------------------+
134
- |
135
- | SQL Queries
136
- v
137
- +---------------------------------------------------------------+
138
- | PostgreSQL Database |
139
- | - Primary (Read/Write) |
140
- | - Replica (Read-only) [optional] |
141
- +---------------------------------------------------------------+
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: 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**:
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
-
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
33
 
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) => { ... })
34
+ export type User = typeof users.$inferSelect;
35
+ export type NewUser = typeof users.$inferInsert;
447
36
  ```
448
37
 
449
- **Key Components**:
38
+ ### 2. Create Repository
450
39
 
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
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';
455
44
 
456
- **Design Pattern**: Builder pattern (same as routes)
45
+ export class UserRepository extends BaseRepository
46
+ {
47
+ async findById(id: string): Promise<User | null>
48
+ {
49
+ return this._findOne(users, { id });
50
+ }
457
51
 
458
- ---
52
+ async findByEmail(email: string): Promise<User | null>
53
+ {
54
+ return this._findOne(users, { email });
55
+ }
459
56
 
460
- ### 10. Event System (`src/event/`)
57
+ async findAll(): Promise<User[]>
58
+ {
59
+ return this._findMany(users);
60
+ }
461
61
 
462
- **Purpose**: Decoupled pub/sub event system
62
+ async create(data: NewUser): Promise<User>
63
+ {
64
+ return this._create(users, data);
65
+ }
463
66
 
464
- **Architecture**:
67
+ async update(id: string, data: Partial<NewUser>): Promise<User | null>
68
+ {
69
+ return this._updateOne(users, { id }, data);
70
+ }
465
71
 
72
+ async delete(id: string): Promise<User | null>
73
+ {
74
+ return this._deleteOne(users, { id });
75
+ }
76
+ }
466
77
  ```
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
78
 
483
- ---
484
-
485
- ## Type System
486
-
487
- ### End-to-End Type Safety Flow
79
+ ### 3. Define Routes
488
80
 
489
81
  ```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
82
  // src/server/routes/users.ts
504
83
  import { route } from '@spfn/core/route';
84
+ import { Transactional } from '@spfn/core/db';
505
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
+ });
506
94
 
507
95
  export const getUser = route.get('/users/:id')
508
96
  .input({
509
- params: Type.Object({
510
- id: Type.String(),
511
- }),
512
- query: Type.Object({
513
- include: Type.Optional(Type.String()),
514
- }),
97
+ params: Type.Object({ id: Type.String() })
515
98
  })
516
99
  .handler(async (c) => {
517
- const { params, query } = await c.data();
518
- // ^? { params: { id: string }, query: { include?: string } }
100
+ const { params } = await c.data();
101
+ const user = await userRepo.findById(params.id);
519
102
 
520
- const user = await findOne(users, { id: params.id });
521
- // ^? User | null
103
+ if (!user)
104
+ {
105
+ throw new Error('User not found');
106
+ }
522
107
 
523
108
  return user;
524
- // ^? Response type inferred from return value
525
109
  });
526
110
 
527
- // 3. Router Type Export
528
- // src/server/router.ts
529
- export const appRouter = defineRouter({
530
- getUser,
531
- // ... other routes
532
- });
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
+ });
533
123
 
534
- export type AppRouter = typeof appRouter;
535
- // ^? 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);
536
136
 
537
- // 4. Client API Call (Next.js)
538
- // app/users/[id]/page.tsx
539
- import { api } from '@spfn/core/nextjs/server';
137
+ if (!user)
138
+ {
139
+ throw new Error('User not found');
140
+ }
540
141
 
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)
142
+ return user;
143
+ });
546
144
 
547
- // Full type inference chain:
548
- // User type → RouteDef → Router → Client API → return type
549
- ```
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);
550
153
 
551
- ### Type Inference Utilities
154
+ if (!user)
155
+ {
156
+ throw new Error('User not found');
157
+ }
552
158
 
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
- };
159
+ return { success: true };
160
+ });
568
161
  ```
569
162
 
570
- ---
571
-
572
- ## Integration Points
573
-
574
- ### 1. Server ↔ Database: Transaction Context
575
-
576
- **Mechanism**: AsyncLocalStorage propagates transaction across async calls
163
+ ### 4. Configure Server
577
164
 
578
165
  ```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' });
166
+ // src/server/server.config.ts
167
+ import { defineServerConfig, defineRouter } from '@spfn/core/server';
168
+ import * as userRoutes from './routes/users';
586
169
 
587
- // Auto-commit on success, auto-rollback on error
588
- return c.json(post);
170
+ const appRouter = defineRouter({
171
+ ...userRoutes
589
172
  });
590
- ```
591
-
592
- **Why**: No need to pass transaction object through function calls
593
173
 
594
- ---
595
-
596
- ### 2. Server ↔ Client: Type Inference
597
-
598
- **Mechanism**: `typeof appRouter` captures all route types
174
+ export default defineServerConfig()
175
+ .port(8790)
176
+ .routes(appRouter)
177
+ .build();
599
178
 
600
- ```typescript
601
- // Server: Export router type
602
- export const appRouter = defineRouter({ getUser, createUser });
603
179
  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
180
  ```
613
181
 
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
182
+ ### 5. Start Server
621
183
 
622
184
  ```typescript
623
- // app/api/rpc/[routeName]/route.ts
624
- import { appRouter } from '@/server/router';
625
- import { createRpcProxy } from '@spfn/core/nextjs/server';
185
+ // src/server/index.ts
186
+ import { startServer } from '@spfn/core/server';
626
187
 
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
188
+ await startServer();
634
189
  ```
635
190
 
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
- ---
191
+ ### 6. Environment Variables
671
192
 
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
- });
193
+ ```bash
194
+ # .env
195
+ DATABASE_URL=postgresql://localhost:5432/mydb
196
+ PORT=8790
696
197
  ```
697
198
 
698
199
  ---
699
200
 
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?
201
+ ## Architecture Overview
718
202
 
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
203
  ```
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) => { ... });
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
+ └─────────────────────────────────────────┘
819
218
  ```
820
219
 
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';
831
-
832
- export const { GET, POST } = createRpcProxy({
833
- router: appRouter,
834
- interceptors: [
835
- {
836
- pathPattern: '/admin/*',
837
- request: async (ctx, next) => {
838
- // Check admin role
839
- const isAdmin = await checkAdminRole(ctx.cookies);
840
- if (!isAdmin) {
841
- throw new Error('Unauthorized');
842
- }
843
- await next();
844
- },
845
- },
846
- ],
847
- });
848
- ```
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
849
224
 
850
225
  ---
851
226
 
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';
227
+ ## Directory Structure
859
228
 
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
229
  ```
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
- }
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
902
242
  ```
903
243
 
904
244
  ---
905
245
 
906
- ## Migration Guides
907
-
908
- ### From Contract-Based to define-route
246
+ ## Core Concepts
909
247
 
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
- ```
248
+ ### Route Definition
926
249
 
927
- **After** (Current):
928
250
  ```typescript
929
- // routes/users.ts
930
251
  import { route } from '@spfn/core/route';
252
+ import { Type } from '@sinclair/typebox';
931
253
 
932
- export const getUser = route.get('/users/:id')
254
+ // GET with params and query
255
+ route.get('/users/:id')
933
256
  .input({
934
257
  params: Type.Object({ id: Type.String() }),
258
+ query: Type.Object({ include: Type.Optional(Type.String()) })
935
259
  })
936
260
  .handler(async (c) => {
937
- const { params } = await c.data();
938
- return { id: params.id, name: 'John' };
261
+ const { params, query } = await c.data();
262
+ // params.id, query.include are fully typed
939
263
  });
940
264
 
941
- // router.ts
942
- import { defineRouter } from '@spfn/core/route';
943
- export const appRouter = defineRouter({ getUser });
944
- 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
+ });
945
277
  ```
946
278
 
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
279
+ ### Repository Pattern
955
280
 
956
- **Before** (Deprecated):
957
281
  ```typescript
958
- // routes/users/[id].ts
959
- export default async function handler(c) { ... }
282
+ import { BaseRepository } from '@spfn/core/db';
960
283
 
961
- // Automatic file system scanning
962
- ```
963
-
964
- **After** (Current):
965
- ```typescript
966
- // routes/users.ts
967
- export const getUser = route.get('/users/:id')...
968
-
969
- // router.ts
970
- 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
971
290
 
972
- // server.config.ts
973
- export default defineServerConfig()
974
- .routes(appRouter)
975
- .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
+ }
976
300
  ```
977
301
 
978
- **Benefits**:
979
- - Explicit route registration
980
- - Better IDE support
981
- - No runtime file system scanning
302
+ ### Transaction
982
303
 
983
- ---
984
-
985
- ### From ContractClient to tRPC-Style API
986
-
987
- **Before** (Old Client):
988
304
  ```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
- ```
305
+ import { Transactional } from '@spfn/core/db';
1006
306
 
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
- ```
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
+ });
1026
314
 
1027
- ### Route System
315
+ // Manual control
316
+ import { runWithTransaction } from '@spfn/core/db';
1028
317
 
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';
318
+ await runWithTransaction(async () => {
319
+ await userRepo.create(userData);
320
+ await profileRepo.create(profileData);
321
+ });
1040
322
  ```
1041
323
 
1042
- ### Database System
324
+ ### Schema Helpers
1043
325
 
1044
326
  ```typescript
1045
327
  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,
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
1062
336
  } 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
337
 
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';
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
+ });
1162
347
  ```
1163
348
 
1164
349
  ---
1165
350
 
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
351
+ ## Module Documentation
1181
352
 
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
- ```
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 |
1204
369
 
1205
370
  ---
1206
371
 
1207
- ### Basic Setup
372
+ ## CLI Commands
1208
373
 
1209
- **1. Install**
1210
374
  ```bash
1211
- npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
1212
- ```
375
+ # Migration
376
+ npx spfn db generate # Generate migration
377
+ npx spfn db migrate # Apply migration
1213
378
 
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
- ```
379
+ # Development
380
+ pnpm spfn:dev # Start dev server (auto codegen)
1224
381
 
1225
- **3. Create Router**
1226
- ```typescript
1227
- // src/server/router.ts
1228
- export const appRouter = defineRouter({ getUser });
1229
- export type AppRouter = typeof appRouter;
1230
- ```
382
+ # API Client Generation
383
+ pnpm spfn codegen run
1231
384
 
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';
1245
- import { createRpcProxy } from '@spfn/core/nextjs/server';
1246
-
1247
- export const { GET, POST } = createRpcProxy({ router: appRouter });
1248
- ```
1249
-
1250
- **6. Use Client**
1251
- ```typescript
1252
- // app/users/[id]/page.tsx
1253
- import { createApi } from '@spfn/core/nextjs';
1254
- import type { AppRouter } from '@/server/router';
1255
-
1256
- const api = createApi<AppRouter>();
1257
- const user = await api.getUser.call({ params: { id: '123' } });
385
+ # Database Studio
386
+ pnpm drizzle-kit studio
1258
387
  ```
1259
388
 
1260
389
  ---
1261
390
 
1262
- ## Documentation
1263
-
1264
- ### Technical Architecture
1265
- - [Route System](./src/route/README.md) - define-route system, type inference
1266
- - [Server System](./src/server/README.md) - Configuration, middleware pipeline, lifecycle
1267
- - [Database System](./src/db/README.md) - Helper functions, transactions, schema helpers
1268
- - [Client System](./src/nextjs/README.md) - tRPC-style API, TypedProxy, interceptors
1269
-
1270
- ### Guides
1271
- - [Transaction Management](./src/db/docs/transactions.md)
1272
- - [Error Handling](./src/errors/README.md)
1273
- - [Logger](./src/logger/README.md)
1274
- - [Cache](./src/cache/README.md)
1275
- - [Middleware](./src/middleware/README.md)
1276
- - [Code Generation](./src/codegen/README.md)
1277
-
1278
- ---
1279
-
1280
- ## Requirements
1281
-
1282
- - Node.js >= 18
1283
- - Next.js 15+ with App Router (when using client integration)
1284
- - PostgreSQL
1285
- - Valkey/Redis (optional)
1286
- - TypeScript >= 5.0
1287
-
1288
- ---
1289
-
1290
- ## Testing
1291
-
1292
- ```bash
1293
- npm test # Run all tests
1294
- npm test -- route # Run route tests only
1295
- npm test -- --coverage # With coverage
1296
- ```
1297
-
1298
- **Test Coverage**: 650+ tests across all modules
1299
-
1300
- ---
1301
-
1302
391
  ## License
1303
392
 
1304
393
  MIT
1305
-
1306
- ---
1307
-
1308
- Part of the [Superfunction Framework](https://github.com/spfn/spfn)