@spfn/cli 0.0.9 → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ # SPFN Best Practices
2
+
3
+ ## Core Philosophy
4
+
5
+ ### 1. **Shared Types Between Next.js ↔ Server**
6
+
7
+ The most important philosophy of SPFN is **sharing the same types between frontend and backend**.
8
+
9
+ **❌ Wrong Approach (Type Duplication):**
10
+ ```typescript
11
+ // Backend: src/server/routes/users/index.ts
12
+ interface User { // ❌ Defined only in server
13
+ id: number;
14
+ name: string;
15
+ }
16
+
17
+ // Frontend: src/app/users/page.tsx
18
+ interface User { // ❌ Same type redefined
19
+ id: number;
20
+ name: string;
21
+ }
22
+ ```
23
+
24
+ **✅ Correct Approach (Shared Types):**
25
+ ```typescript
26
+ // Shared: src/types/user.ts
27
+ export interface User {
28
+ id: number;
29
+ name: string;
30
+ email: string;
31
+ createdAt: Date;
32
+ }
33
+
34
+ export interface CreateUserRequest {
35
+ name: string;
36
+ email: string;
37
+ }
38
+
39
+ // Backend: src/server/routes/users/index.ts
40
+ import type { User, CreateUserRequest } from '@/types/user';
41
+
42
+ export async function POST(c: RouteContext) {
43
+ const data = await c.data<CreateUserRequest>();
44
+ // ...
45
+ return c.json<User>(newUser);
46
+ }
47
+
48
+ // Frontend: src/app/users/page.tsx
49
+ import type { User } from '@/types/user';
50
+ import { api } from '@/lib/api'; // Auto-generated!
51
+
52
+ async function getUsers(): Promise<User[]> {
53
+ return api.users.getUsers(); // Type-safe & auto-generated
54
+ }
55
+ ```
56
+
57
+ ---
58
+
59
+ ### 2. **RouteContext Usage (Hono Wrapping Strategy)**
60
+
61
+ SPFN **wraps** Hono's Context to provide a more convenient API, while keeping full access to Hono's original Context via `c.raw`.
62
+
63
+ #### ❌ Don't Use `c.req` Directly
64
+ ```typescript
65
+ // This DOES NOT work in SPFN!
66
+ export async function POST(c: RouteContext) {
67
+ const body = await c.req.json(); // ❌ Error: 'req' doesn't exist on RouteContext
68
+ return c.json({ ... });
69
+ }
70
+ ```
71
+
72
+ #### ✅ Use SPFN's Wrapped API
73
+ ```typescript
74
+ import type { RouteContext } from '@spfn/core';
75
+
76
+ export async function POST(c: RouteContext) {
77
+ // ✅ Request Body (SPFN wrapper)
78
+ const body = await c.data<CreateUserRequest>();
79
+
80
+ // ✅ Path Parameters (SPFN wrapper)
81
+ const userId = c.params.id; // /users/:id
82
+
83
+ // ✅ Query Parameters (SPFN wrapper)
84
+ const page = c.query.page; // /users?page=1
85
+
86
+ // ✅ Pageable (SPFN-only feature, Spring Boot style)
87
+ const { filters, sort, pagination } = c.pageable;
88
+
89
+ // ✅ JSON Response (same as Hono)
90
+ return c.json<User>(result);
91
+ }
92
+ ```
93
+
94
+ #### ✅ Access Original Hono Context via `c.raw`
95
+ When you need Hono's native features, use `c.raw`:
96
+
97
+ ```typescript
98
+ export async function POST(c: RouteContext) {
99
+ // ✅ Headers (via c.raw)
100
+ const authHeader = c.raw.req.header('Authorization');
101
+ const contentType = c.raw.req.header('Content-Type');
102
+
103
+ // ✅ Cookies (via c.raw)
104
+ const sessionToken = c.raw.req.cookie('session');
105
+
106
+ // ✅ Set Response Headers (via c.raw)
107
+ c.raw.header('X-Custom-Header', 'value');
108
+ c.raw.header('Cache-Control', 'no-cache');
109
+
110
+ // ✅ Context Variables - Share data between middlewares (via c.raw)
111
+ const userId = c.raw.get('userId');
112
+ c.raw.set('requestId', crypto.randomUUID());
113
+
114
+ // ✅ File Upload (via c.raw)
115
+ const formData = await c.raw.req.formData();
116
+ const file = formData.get('file');
117
+
118
+ // ✅ Raw Request object (via c.raw)
119
+ const method = c.raw.req.method;
120
+ const url = c.raw.req.url;
121
+
122
+ // You can still use SPFN's wrapped methods
123
+ const body = await c.data<CreateUserRequest>();
124
+
125
+ return c.json({ success: true });
126
+ }
127
+ ```
128
+
129
+ **Summary:**
130
+ - **SPFN wrappers**: `c.data()`, `c.params`, `c.query`, `c.pageable`, `c.json()`
131
+ - **Hono original**: Everything via `c.raw` (e.g., `c.raw.req.header()`, `c.raw.req.cookie()`)
132
+ - **Both work together**: Use SPFN for convenience, `c.raw` for advanced Hono features
133
+
134
+ ---
135
+
136
+ ### 3. **Auto-Generated API Client**
137
+
138
+ SPFN automatically generates type-safe API clients from your routes.
139
+
140
+ #### Setup
141
+
142
+ Add to `package.json`:
143
+ ```json
144
+ {
145
+ "scripts": {
146
+ "generate": "tsx node_modules/@spfn/core/dist/scripts/watch-all.js",
147
+ "generate:api": "tsx node_modules/@spfn/core/dist/scripts/generate-api-client.js",
148
+ "generate:types": "tsx node_modules/@spfn/core/dist/scripts/generate-types.js"
149
+ }
150
+ }
151
+ ```
152
+
153
+ #### Watch Mode (Recommended)
154
+ ```bash
155
+ npm run generate
156
+ ```
157
+
158
+ This watches for changes and auto-regenerates:
159
+ - **Routes change** → API client regenerates (`src/lib/api/`)
160
+ - **Entities change** → Types regenerate (after migration)
161
+
162
+ #### Manual Generation
163
+ ```bash
164
+ npm run generate:api # Generate API client
165
+ npm run generate:types # Generate types from entities
166
+ ```
167
+
168
+ #### Generated API Client Usage
169
+
170
+ **Backend Route:**
171
+ ```typescript
172
+ // src/server/routes/users/index.ts
173
+ import type { User, CreateUserRequest } from '@/types/user';
174
+
175
+ export const meta = { tags: ['users'] };
176
+
177
+ export async function GET(c: RouteContext) {
178
+ const users = await db.select().from(usersTable);
179
+ return c.json<User[]>(users);
180
+ }
181
+
182
+ export async function POST(c: RouteContext) {
183
+ const data = await c.data<CreateUserRequest>();
184
+ const [user] = await db.insert(usersTable).values(data).returning();
185
+ return c.json<User>(user);
186
+ }
187
+ ```
188
+
189
+ **Auto-Generated Client:**
190
+ ```typescript
191
+ // src/lib/api/users.ts (auto-generated)
192
+ export const users = {
193
+ getUsers: () => request<User[]>('/users'),
194
+ createUsers: (data: CreateUserRequest) => request<User>('/users', 'POST', data),
195
+ };
196
+ ```
197
+
198
+ **Frontend Usage:**
199
+ ```typescript
200
+ // src/app/users/page.tsx
201
+ import { api } from '@/lib/api';
202
+ import type { User } from '@/types/user';
203
+
204
+ export default async function UsersPage() {
205
+ // ✅ Type-safe, auto-completed
206
+ const users = await api.users.getUsers();
207
+
208
+ return (
209
+ <ul>
210
+ {users.map(user => (
211
+ <li key={user.id}>{user.name}</li>
212
+ ))}
213
+ </ul>
214
+ );
215
+ }
216
+ ```
217
+
218
+ ---
219
+
220
+ ### 4. **Recommended Project Structure**
221
+
222
+ ```
223
+ your-nextjs-project/
224
+ ├── src/
225
+ │ ├── app/ # Next.js App Router (Frontend)
226
+ │ │ ├── users/
227
+ │ │ │ └── page.tsx
228
+ │ │ └── layout.tsx
229
+ │ │
230
+ │ ├── types/ # 🔥 Shared Types (Frontend ↔ Backend)
231
+ │ │ ├── user.ts # User-related types
232
+ │ │ ├── post.ts # Post-related types
233
+ │ │ └── api.ts # API Request/Response types
234
+ │ │
235
+ │ ├── lib/ # Frontend utilities
236
+ │ │ └── api/ # ⚡ Auto-generated API clients
237
+ │ │ ├── index.ts # Aggregated exports
238
+ │ │ ├── users.ts # User API client
239
+ │ │ └── posts.ts # Post API client
240
+ │ │
241
+ │ └── server/ # SPFN Backend
242
+ │ ├── routes/ # API Routes
243
+ │ │ ├── users/
244
+ │ │ │ ├── index.ts # GET /users, POST /users
245
+ │ │ │ └── [id].ts # GET /users/:id
246
+ │ │ └── posts/
247
+ │ │ └── index.ts
248
+ │ │
249
+ │ ├── entities/ # Drizzle Entities
250
+ │ │ └── users.ts
251
+ │ │
252
+ │ ├── middleware/ # Custom Middleware (optional)
253
+ │ │ └── auth.ts
254
+ │ │
255
+ │ └── server.config.ts # Server Config (optional)
256
+
257
+ └── package.json
258
+ ```
259
+
260
+ ---
261
+
262
+ ### 5. **Entities vs Types**
263
+
264
+ #### Entity = Database Schema
265
+ ```typescript
266
+ // src/server/entities/users.ts
267
+ import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';
268
+
269
+ export const users = pgTable('users', {
270
+ id: serial('id').primaryKey(),
271
+ name: varchar('name', { length: 100 }).notNull(),
272
+ email: varchar('email', { length: 255 }).notNull().unique(),
273
+ createdAt: timestamp('created_at').defaultNow(),
274
+ });
275
+ ```
276
+
277
+ #### Type = API Contract (Shared with Frontend)
278
+ ```typescript
279
+ // src/types/user.ts (auto-generated or manual)
280
+ export interface User {
281
+ id: number;
282
+ name: string;
283
+ email: string;
284
+ createdAt: Date;
285
+ }
286
+
287
+ export interface CreateUserRequest {
288
+ name: string;
289
+ email: string;
290
+ }
291
+ ```
292
+
293
+ ---
294
+
295
+ ### 6. **TypeScript Path Aliases**
296
+
297
+ Simplify imports with `tsconfig.json`:
298
+
299
+ ```json
300
+ {
301
+ "compilerOptions": {
302
+ "paths": {
303
+ "@/*": ["./src/*"],
304
+ "@/types/*": ["./src/types/*"],
305
+ "@/server/*": ["./src/server/*"],
306
+ "@/lib/*": ["./src/lib/*"]
307
+ }
308
+ }
309
+ }
310
+ ```
311
+
312
+ Usage:
313
+ ```typescript
314
+ // ❌ Relative paths (avoid)
315
+ import { User } from '../../../types/user';
316
+ import { users } from '../../entities/users';
317
+
318
+ // ✅ Use aliases
319
+ import type { User } from '@/types/user';
320
+ import { users } from '@/server/entities/users';
321
+ import { api } from '@/lib/api';
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Quick Reference
327
+
328
+ ### RouteContext API Comparison
329
+
330
+ | Feature | SPFN Wrapper | Hono Original (via c.raw) |
331
+ |---------|--------------|---------------------------|
332
+ | Request Body | `c.data<T>()` | `c.raw.req.json()` |
333
+ | Path Params | `c.params.id` | `c.raw.req.param('id')` |
334
+ | Query Params | `c.query.page` | `c.raw.req.query('page')` |
335
+ | Headers | `c.raw.req.header('...')` | ✅ |
336
+ | Cookies | `c.raw.req.cookie('...')` | ✅ |
337
+ | Response | `c.json()` | `c.raw.json()` |
338
+ | Pagination | `c.pageable` | ❌ (SPFN-only) |
339
+ | Context Variables | `c.raw.get()` / `c.raw.set()` | ✅ |
340
+ | Raw Request | `c.raw.req` | ✅ |
341
+
342
+ ### Why SPFN Wraps Hono Context
343
+
344
+ **Benefits:**
345
+ - 🎯 Simpler API for common operations
346
+ - 🔒 Type-safe request parsing with `c.data<T>()`
347
+ - 📊 Built-in pagination support (`c.pageable`)
348
+ - 🚀 Spring Boot-inspired developer experience
349
+ - 🔓 **Full Hono access via `c.raw`** - No limitations!
350
+
351
+ **Example - Combining Both:**
352
+ ```typescript
353
+ export async function POST(c: RouteContext) {
354
+ // SPFN wrapper (simple & type-safe)
355
+ const body = await c.data<CreateUserRequest>();
356
+ const userId = c.params.id;
357
+
358
+ // Hono original (advanced features)
359
+ const authToken = c.raw.req.header('Authorization');
360
+ c.raw.set('userId', userId);
361
+
362
+ return c.json({ success: true });
363
+ }
364
+ ```
365
+
366
+ ---
367
+
368
+ ### Auto-Generated API Client Commands
369
+
370
+ ```bash
371
+ # Start watch mode (recommended for development)
372
+ npm run generate
373
+
374
+ # Manually generate API client from routes
375
+ npm run generate:api
376
+
377
+ # Manually generate types from entities
378
+ npm run generate:types
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Learn More
384
+
385
+ - [SPFN Core Documentation](https://spfn.dev/docs)
386
+ - [Hono Documentation](https://hono.dev) - Full Hono API available via `c.raw`
387
+ - [Next.js Documentation](https://nextjs.org/docs)
388
+ - [Drizzle ORM](https://orm.drizzle.team/)
@@ -0,0 +1,131 @@
1
+ # Entities
2
+
3
+ Define your Drizzle ORM entities here. These are your database table schemas.
4
+
5
+ Use `spfn generate <entity-name>` to scaffold a new entity with CRUD routes.
6
+
7
+ ## Entity Helper Functions
8
+
9
+ SPFN provides helper functions to reduce boilerplate when defining entities:
10
+
11
+ ```typescript
12
+ import { pgTable, text } from 'drizzle-orm/pg-core';
13
+ import { id, timestamps, foreignKey } from '@spfn/core';
14
+
15
+ // Simple entity with helpers
16
+ export const users = pgTable('users', {
17
+ id: id(), // bigserial primary key
18
+ email: text('email').unique(),
19
+ ...timestamps(), // createdAt + updatedAt
20
+ });
21
+
22
+ // Entity with foreign key
23
+ export const posts = pgTable('posts', {
24
+ id: id(),
25
+ title: text('title').notNull(),
26
+ authorId: foreignKey('author', () => users.id), // Cascade delete
27
+ ...timestamps(),
28
+ });
29
+
30
+ export type User = typeof users.$inferSelect;
31
+ export type Post = typeof posts.$inferSelect;
32
+ ```
33
+
34
+ **Available helpers:**
35
+ - `id()` - Auto-incrementing bigserial primary key
36
+ - `timestamps()` - Adds `createdAt` and `updatedAt` fields
37
+ - `foreignKey(name, ref)` - Foreign key with cascade delete
38
+ - `optionalForeignKey(name, ref)` - Nullable foreign key
39
+
40
+ ## Pattern 1: Simple Route (No Repository)
41
+
42
+ Use `getDb()` directly in routes for simple queries:
43
+
44
+ ```typescript
45
+ // src/server/routes/users/index.ts
46
+ import type { RouteContext } from '@spfn/core';
47
+ import { getDb } from '@spfn/core';
48
+ import { users } from '../../entities/users.js';
49
+
50
+ export async function GET(c: RouteContext)
51
+ {
52
+ const db = getDb();
53
+ const allUsers = await db.select().from(users);
54
+ return c.json(allUsers);
55
+ }
56
+ ```
57
+
58
+ ## Pattern 2: Repository Pattern (Recommended)
59
+
60
+ For complex business logic, use the Repository pattern:
61
+
62
+ **1. Create a repository:**
63
+
64
+ ```typescript
65
+ // src/server/repositories/user.repository.ts
66
+ import { eq } from 'drizzle-orm';
67
+ import { BaseRepository } from '@spfn/core';
68
+ import { users, type User, type NewUser } from '../entities/users.js';
69
+
70
+ export class UserRepository extends BaseRepository<typeof users, User, NewUser>
71
+ {
72
+ constructor()
73
+ {
74
+ super(users);
75
+ }
76
+
77
+ async findByEmail(email: string): Promise<User | undefined>
78
+ {
79
+ const db = await this.getDb();
80
+ const result = await db.select().from(users).where(eq(users.email, email));
81
+ return result[0];
82
+ }
83
+ }
84
+ ```
85
+
86
+ **2. Use in routes with automatic transactions:**
87
+
88
+ ```typescript
89
+ // src/server/routes/users/POST.ts
90
+ import type { RouteContext } from '@spfn/core';
91
+ import { Transactional } from '@spfn/core';
92
+ import { UserRepository } from '../../repositories/user.repository.js';
93
+
94
+ const userRepo = new UserRepository();
95
+
96
+ // POST /users - Transaction automatically managed by Transactional() middleware
97
+ export const POST = [
98
+ Transactional(),
99
+ async (c: RouteContext) =>
100
+ {
101
+ const data = await c.req.json();
102
+ const user = await userRepo.create(data);
103
+ return c.json(user, 201);
104
+ }
105
+ ];
106
+ ```
107
+
108
+ ## @spfn/core Features
109
+
110
+ - **Zero-Config**: Framework auto-loads routes from `routes/` directory
111
+ - **File-based Routing**: Next.js App Router style (GET.ts, POST.ts, [id].ts, etc.)
112
+ - **Repository Pattern**: Spring Data JPA style with `BaseRepository`
113
+ - **Automatic Transactions**: `Transactional()` middleware with AsyncLocalStorage
114
+ - **Type-safe DB Access**: `getDb()` automatically uses transaction context when available
115
+
116
+ ## Database Migration
117
+
118
+ ```bash
119
+ # Generate migration from your entities
120
+ npx drizzle-kit generate:pg
121
+
122
+ # Run migrations
123
+ npx drizzle-kit push:pg
124
+ ```
125
+
126
+ ## Learn More
127
+
128
+ - [Getting Started](https://spfn.dev/docs/getting-started)
129
+ - [Routing Guide](https://spfn.dev/docs/routing)
130
+ - [Repository Pattern](https://spfn.dev/docs/repository)
131
+ - [Transaction Management](https://spfn.dev/docs/transactions)
@@ -0,0 +1,101 @@
1
+ import { Type } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * Example Contracts
5
+ *
6
+ * Demonstrates various contract patterns
7
+ */
8
+
9
+ /**
10
+ * GET /examples - List examples
11
+ */
12
+ export const getExamplesContract = {
13
+ method: 'GET' as const,
14
+ path: '/',
15
+ query: Type.Object({
16
+ limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
17
+ offset: Type.Optional(Type.Number({ minimum: 0 }))
18
+ }),
19
+ response: Type.Object({
20
+ examples: Type.Array(Type.Object({
21
+ id: Type.String(),
22
+ name: Type.String(),
23
+ description: Type.String()
24
+ })),
25
+ total: Type.Number(),
26
+ limit: Type.Number(),
27
+ offset: Type.Number()
28
+ })
29
+ };
30
+
31
+ /**
32
+ * GET /examples/:id - Get single example
33
+ */
34
+ export const getExampleContract = {
35
+ method: 'GET' as const,
36
+ path: '/:id',
37
+ params: Type.Object({
38
+ id: Type.String()
39
+ }),
40
+ response: Type.Object({
41
+ id: Type.String(),
42
+ name: Type.String(),
43
+ description: Type.String(),
44
+ createdAt: Type.Number(),
45
+ updatedAt: Type.Number()
46
+ })
47
+ };
48
+
49
+ /**
50
+ * POST /examples - Create example
51
+ */
52
+ export const createExampleContract = {
53
+ method: 'POST' as const,
54
+ path: '/',
55
+ body: Type.Object({
56
+ name: Type.String(),
57
+ description: Type.String()
58
+ }),
59
+ response: Type.Object({
60
+ id: Type.String(),
61
+ name: Type.String(),
62
+ description: Type.String(),
63
+ createdAt: Type.Number()
64
+ })
65
+ };
66
+
67
+ /**
68
+ * PUT /examples/:id - Update example
69
+ */
70
+ export const updateExampleContract = {
71
+ method: 'PUT' as const,
72
+ path: '/:id',
73
+ params: Type.Object({
74
+ id: Type.String()
75
+ }),
76
+ body: Type.Object({
77
+ name: Type.Optional(Type.String()),
78
+ description: Type.Optional(Type.String())
79
+ }),
80
+ response: Type.Object({
81
+ id: Type.String(),
82
+ name: Type.String(),
83
+ description: Type.String(),
84
+ updatedAt: Type.Number()
85
+ })
86
+ };
87
+
88
+ /**
89
+ * DELETE /examples/:id - Delete example
90
+ */
91
+ export const deleteExampleContract = {
92
+ method: 'DELETE' as const,
93
+ path: '/:id',
94
+ params: Type.Object({
95
+ id: Type.String()
96
+ }),
97
+ response: Type.Object({
98
+ success: Type.Boolean(),
99
+ id: Type.String()
100
+ })
101
+ };