@spfn/core 0.2.0-beta.49 → 0.2.0-beta.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +181 -366
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +7 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +2 -3
- package/dist/env/loader.js +0 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.js.map +1 -1
- package/dist/event/ws/index.js.map +1 -1
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -3
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -436
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -499
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -443
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -247
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -429
package/docs/middleware.md
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
# Middleware
|
|
2
|
-
|
|
3
|
-
Named middleware system with route-level skip control.
|
|
4
|
-
|
|
5
|
-
## Define Middleware
|
|
6
|
-
|
|
7
|
-
### Regular Middleware
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
import { defineMiddleware } from '@spfn/core/route';
|
|
11
|
-
|
|
12
|
-
export const authMiddleware = defineMiddleware('auth', async (c, next) => {
|
|
13
|
-
const token = c.req.header('authorization');
|
|
14
|
-
|
|
15
|
-
if (!token)
|
|
16
|
-
{
|
|
17
|
-
return c.json({ error: 'Unauthorized' }, 401);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const user = await verifyToken(token);
|
|
21
|
-
c.set('user', user);
|
|
22
|
-
|
|
23
|
-
await next();
|
|
24
|
-
});
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Factory Middleware
|
|
28
|
-
|
|
29
|
-
Middleware with parameters:
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
export const requireRole = defineMiddleware('role',
|
|
33
|
-
(...roles: string[]) => async (c, next) => {
|
|
34
|
-
const user = c.get('user');
|
|
35
|
-
|
|
36
|
-
if (!roles.includes(user.role))
|
|
37
|
-
{
|
|
38
|
-
return c.json({ error: 'Forbidden' }, 403);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await next();
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// Usage
|
|
46
|
-
route.get('/admin')
|
|
47
|
-
.use([requireRole('admin', 'superadmin')])
|
|
48
|
-
.handler(...)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Two-Parameter Factory
|
|
52
|
-
|
|
53
|
-
For factory with exactly 2 parameters, use `defineMiddlewareFactory`:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { defineMiddlewareFactory } from '@spfn/core/route';
|
|
57
|
-
|
|
58
|
-
export const rateLimiter = defineMiddlewareFactory('rateLimit',
|
|
59
|
-
(limit: number, windowMs: number) => async (c, next) => {
|
|
60
|
-
// Rate limit logic
|
|
61
|
-
await next();
|
|
62
|
-
}
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Usage
|
|
66
|
-
route.get('/api')
|
|
67
|
-
.use([rateLimiter(100, 60000)]) // 100 requests per minute
|
|
68
|
-
.handler(...)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## Register Global Middleware
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// src/server/server.config.ts
|
|
77
|
-
import { defineServerConfig } from '@spfn/core/server';
|
|
78
|
-
import { authMiddleware, loggerMiddleware, corsMiddleware } from './middlewares';
|
|
79
|
-
|
|
80
|
-
export default defineServerConfig()
|
|
81
|
-
.middlewares([
|
|
82
|
-
loggerMiddleware,
|
|
83
|
-
corsMiddleware,
|
|
84
|
-
authMiddleware
|
|
85
|
-
])
|
|
86
|
-
.routes(appRouter)
|
|
87
|
-
.build();
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Execution order:**
|
|
91
|
-
1. Global middlewares (in registration order)
|
|
92
|
-
2. Route-level middlewares (from `.use()`)
|
|
93
|
-
3. Validation middleware (automatic)
|
|
94
|
-
4. Route handler
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Use in Routes
|
|
99
|
-
|
|
100
|
-
### Add Middleware
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { Transactional } from '@spfn/core/db';
|
|
104
|
-
import { authMiddleware, requireRole } from './middlewares';
|
|
105
|
-
|
|
106
|
-
route.post('/admin/users')
|
|
107
|
-
.use([authMiddleware, requireRole('admin'), Transactional()])
|
|
108
|
-
.handler(async (c) => {
|
|
109
|
-
const user = c.raw.get('user'); // From authMiddleware
|
|
110
|
-
// ...
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Skip Global Middleware
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
// Skip specific middlewares by name
|
|
118
|
-
route.get('/public/health')
|
|
119
|
-
.skip(['auth', 'rateLimit'])
|
|
120
|
-
.handler(async (c) => {
|
|
121
|
-
return { status: 'ok' };
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Skip all global middlewares
|
|
125
|
-
route.get('/webhooks/stripe')
|
|
126
|
-
.skip('*')
|
|
127
|
-
.handler(async (c) => {
|
|
128
|
-
// No global middleware applied
|
|
129
|
-
});
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**Note:** Route-level middlewares (`.use()`) are never skipped.
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Common Middleware Patterns
|
|
137
|
-
|
|
138
|
-
### Authentication
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
export const authMiddleware = defineMiddleware('auth', async (c, next) => {
|
|
142
|
-
const token = c.req.header('authorization')?.replace('Bearer ', '');
|
|
143
|
-
|
|
144
|
-
if (!token)
|
|
145
|
-
{
|
|
146
|
-
return c.json({ error: 'Missing token' }, 401);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try
|
|
150
|
-
{
|
|
151
|
-
const payload = await verifyJWT(token);
|
|
152
|
-
c.set('userId', payload.sub);
|
|
153
|
-
c.set('user', await userRepo.findById(payload.sub));
|
|
154
|
-
await next();
|
|
155
|
-
}
|
|
156
|
-
catch
|
|
157
|
-
{
|
|
158
|
-
return c.json({ error: 'Invalid token' }, 401);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Role-based Access
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
export const requirePermissions = defineMiddleware('permission',
|
|
167
|
-
(...permissions: string[]) => async (c, next) => {
|
|
168
|
-
const user = c.get('user');
|
|
169
|
-
|
|
170
|
-
const hasPermission = permissions.every(p =>
|
|
171
|
-
user.permissions.includes(p)
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
if (!hasPermission)
|
|
175
|
-
{
|
|
176
|
-
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
await next();
|
|
180
|
-
}
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// Usage
|
|
184
|
-
route.delete('/posts/:id')
|
|
185
|
-
.use([requirePermissions('posts:delete')])
|
|
186
|
-
.handler(...)
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Rate Limiting
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
const requestCounts = new Map<string, { count: number; resetAt: number }>();
|
|
193
|
-
|
|
194
|
-
export const rateLimiter = defineMiddlewareFactory('rateLimit',
|
|
195
|
-
(limit: number, windowMs: number) => async (c, next) => {
|
|
196
|
-
const key = c.req.header('x-forwarded-for') || 'unknown';
|
|
197
|
-
const now = Date.now();
|
|
198
|
-
|
|
199
|
-
const record = requestCounts.get(key);
|
|
200
|
-
|
|
201
|
-
if (!record || now > record.resetAt)
|
|
202
|
-
{
|
|
203
|
-
requestCounts.set(key, { count: 1, resetAt: now + windowMs });
|
|
204
|
-
}
|
|
205
|
-
else if (record.count >= limit)
|
|
206
|
-
{
|
|
207
|
-
return c.json({ error: 'Too many requests' }, 429);
|
|
208
|
-
}
|
|
209
|
-
else
|
|
210
|
-
{
|
|
211
|
-
record.count++;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
await next();
|
|
215
|
-
}
|
|
216
|
-
);
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Request Logging
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
export const requestLogger = defineMiddleware('requestLogger', async (c, next) => {
|
|
223
|
-
const start = Date.now();
|
|
224
|
-
const method = c.req.method;
|
|
225
|
-
const path = c.req.path;
|
|
226
|
-
|
|
227
|
-
await next();
|
|
228
|
-
|
|
229
|
-
const duration = Date.now() - start;
|
|
230
|
-
const status = c.res.status;
|
|
231
|
-
|
|
232
|
-
console.log(`${method} ${path} ${status} ${duration}ms`);
|
|
233
|
-
});
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### CORS
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
export const corsMiddleware = defineMiddleware('cors', async (c, next) => {
|
|
240
|
-
const origin = c.req.header('origin');
|
|
241
|
-
|
|
242
|
-
if (origin && allowedOrigins.includes(origin))
|
|
243
|
-
{
|
|
244
|
-
c.header('Access-Control-Allow-Origin', origin);
|
|
245
|
-
c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
246
|
-
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (c.req.method === 'OPTIONS')
|
|
250
|
-
{
|
|
251
|
-
return c.body(null, 204);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
await next();
|
|
255
|
-
});
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
---
|
|
259
|
-
|
|
260
|
-
## Middleware Deduplication
|
|
261
|
-
|
|
262
|
-
When the same middleware is registered both globally and in a route, it's automatically deduplicated:
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// Registered globally
|
|
266
|
-
.middlewares([authMiddleware])
|
|
267
|
-
|
|
268
|
-
// Also used in route
|
|
269
|
-
route.get('/users')
|
|
270
|
-
.use([authMiddleware]) // Skipped (already applied globally)
|
|
271
|
-
.handler(...)
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
---
|
|
275
|
-
|
|
276
|
-
## Access Context Data
|
|
277
|
-
|
|
278
|
-
### Set Data
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
defineMiddleware('auth', async (c, next) => {
|
|
282
|
-
c.set('user', user);
|
|
283
|
-
c.set('sessionId', sessionId);
|
|
284
|
-
await next();
|
|
285
|
-
});
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Get Data in Handler
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
route.get('/profile')
|
|
292
|
-
.handler(async (c) => {
|
|
293
|
-
const user = c.raw.get('user');
|
|
294
|
-
const sessionId = c.raw.get('sessionId');
|
|
295
|
-
// ...
|
|
296
|
-
});
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
## Best Practices
|
|
302
|
-
|
|
303
|
-
### Do
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
// 1. Use meaningful names for skip control
|
|
307
|
-
export const authMiddleware = defineMiddleware('auth', ...);
|
|
308
|
-
export const rateLimiter = defineMiddleware('rateLimit', ...);
|
|
309
|
-
|
|
310
|
-
// 2. Set context data for downstream use
|
|
311
|
-
c.set('user', user);
|
|
312
|
-
|
|
313
|
-
// 3. Return early for unauthorized requests
|
|
314
|
-
if (!token) return c.json({ error: 'Unauthorized' }, 401);
|
|
315
|
-
|
|
316
|
-
// 4. Always call next() for successful middleware
|
|
317
|
-
await next();
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Don't
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// 1. Don't forget to call next()
|
|
324
|
-
defineMiddleware('logger', async (c, next) => {
|
|
325
|
-
console.log(c.req.path);
|
|
326
|
-
// Missing next() - request hangs!
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// 2. Don't use generic names
|
|
330
|
-
defineMiddleware('middleware1', ...); // Bad
|
|
331
|
-
|
|
332
|
-
// 3. Don't throw errors - return responses
|
|
333
|
-
defineMiddleware('auth', async (c, next) => {
|
|
334
|
-
if (!token) throw new Error('No token'); // Bad
|
|
335
|
-
if (!token) return c.json({ error: 'No token' }, 401); // Good
|
|
336
|
-
});
|
|
337
|
-
```
|
package/docs/nextjs.md
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
# Next.js Integration
|
|
2
|
-
|
|
3
|
-
RPC proxy and type-safe API client for Next.js.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
### 1. Create RPC Proxy
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
// app/api/rpc/[routeName]/route.ts
|
|
11
|
-
import '@spfn/auth/nextjs/api';
|
|
12
|
-
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
13
|
-
import { authRouteMap } from '@spfn/auth';
|
|
14
|
-
import { eventRouteMap } from '@spfn/core/event';
|
|
15
|
-
import { routeMap } from '@/generated/route-map';
|
|
16
|
-
|
|
17
|
-
export const { GET, POST } = createRpcProxy({
|
|
18
|
-
routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### 2. Create API Client
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
// src/lib/api.ts
|
|
26
|
-
import { createApi } from '@spfn/core/nextjs';
|
|
27
|
-
import type { AppRouter } from '@/server/server.config';
|
|
28
|
-
|
|
29
|
-
export const api = createApi<AppRouter>();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Usage
|
|
33
|
-
|
|
34
|
-
### Server Components
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// app/users/[id]/page.tsx
|
|
38
|
-
import { api } from '@/lib/api';
|
|
39
|
-
|
|
40
|
-
export default async function UserPage({ params }: { params: { id: string } })
|
|
41
|
-
{
|
|
42
|
-
const user = await api.getUser.call({
|
|
43
|
-
params: { id: params.id }
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return <div>{user.name}</div>;
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Client Components
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
'use client';
|
|
54
|
-
|
|
55
|
-
import { api } from '@/lib/api';
|
|
56
|
-
import { useState } from 'react';
|
|
57
|
-
|
|
58
|
-
export function CreateUserForm()
|
|
59
|
-
{
|
|
60
|
-
const [loading, setLoading] = useState(false);
|
|
61
|
-
|
|
62
|
-
async function handleSubmit(formData: FormData)
|
|
63
|
-
{
|
|
64
|
-
setLoading(true);
|
|
65
|
-
try
|
|
66
|
-
{
|
|
67
|
-
await api.createUser.call({
|
|
68
|
-
body: {
|
|
69
|
-
email: formData.get('email') as string,
|
|
70
|
-
name: formData.get('name') as string
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
finally
|
|
75
|
-
{
|
|
76
|
-
setLoading(false);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<form action={handleSubmit}>
|
|
82
|
-
{/* ... */}
|
|
83
|
-
</form>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Server Actions
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
// app/actions.ts
|
|
92
|
-
'use server';
|
|
93
|
-
|
|
94
|
-
import { api } from '@/lib/api';
|
|
95
|
-
|
|
96
|
-
export async function createUser(formData: FormData)
|
|
97
|
-
{
|
|
98
|
-
const user = await api.createUser.call({
|
|
99
|
-
body: {
|
|
100
|
-
email: formData.get('email') as string,
|
|
101
|
-
name: formData.get('name') as string
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return user;
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## API Client Methods
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
// Call with params
|
|
113
|
-
const user = await api.getUser.call({
|
|
114
|
-
params: { id: '123' }
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Call with query
|
|
118
|
-
const users = await api.getUsers.call({
|
|
119
|
-
query: { page: 1, limit: 20, search: 'john' }
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Call with body
|
|
123
|
-
const created = await api.createUser.call({
|
|
124
|
-
body: { email: 'user@example.com', name: 'User' }
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Call with multiple inputs
|
|
128
|
-
const updated = await api.updateUser.call({
|
|
129
|
-
params: { id: '123' },
|
|
130
|
-
body: { name: 'Updated Name' }
|
|
131
|
-
});
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Interceptors
|
|
135
|
-
|
|
136
|
-
### Request Interceptor
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
export const { GET, POST } = createRpcProxy({
|
|
140
|
-
routeMap: { ...routeMap, ...authRouteMap },
|
|
141
|
-
apiUrl: process.env.SPFN_API_URL,
|
|
142
|
-
interceptors: {
|
|
143
|
-
request: async (request, context) => {
|
|
144
|
-
// Add auth header
|
|
145
|
-
const token = cookies().get('token')?.value;
|
|
146
|
-
if (token)
|
|
147
|
-
{
|
|
148
|
-
request.headers.set('Authorization', `Bearer ${token}`);
|
|
149
|
-
}
|
|
150
|
-
return request;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Response Interceptor
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
interceptors: {
|
|
160
|
-
response: async (response, context) => {
|
|
161
|
-
// Handle Set-Cookie from API
|
|
162
|
-
const setCookie = response.headers.get('set-cookie');
|
|
163
|
-
if (setCookie)
|
|
164
|
-
{
|
|
165
|
-
cookies().set(parseCookie(setCookie));
|
|
166
|
-
}
|
|
167
|
-
return response;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Cookie Handling
|
|
173
|
-
|
|
174
|
-
The RPC proxy automatically handles HttpOnly cookies:
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
// Server sets cookie
|
|
178
|
-
c.header('Set-Cookie', 'session=abc; HttpOnly; Secure');
|
|
179
|
-
|
|
180
|
-
// Proxy forwards to browser
|
|
181
|
-
// Browser stores HttpOnly cookie
|
|
182
|
-
// Subsequent requests include cookie automatically
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Error Handling
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
try
|
|
189
|
-
{
|
|
190
|
-
const user = await api.getUser.call({ params: { id: '123' } });
|
|
191
|
-
}
|
|
192
|
-
catch (error)
|
|
193
|
-
{
|
|
194
|
-
if (error.status === 404)
|
|
195
|
-
{
|
|
196
|
-
// Not found
|
|
197
|
-
}
|
|
198
|
-
else if (error.status === 401)
|
|
199
|
-
{
|
|
200
|
-
// Unauthorized
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## Environment Variables
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
# API server URL
|
|
209
|
-
SPFN_API_URL=http://localhost:8790
|
|
210
|
-
|
|
211
|
-
# For production
|
|
212
|
-
SPFN_API_URL=https://api.example.com
|
|
213
|
-
|
|
214
|
-
# RPC proxy timeout (AbortController, default: 120s)
|
|
215
|
-
# Should be shorter than FETCH_HEADERS_TIMEOUT for meaningful 504 responses
|
|
216
|
-
RPC_PROXY_TIMEOUT=120000
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## Best Practices
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
// 1. Create single api instance
|
|
223
|
-
// src/lib/api.ts
|
|
224
|
-
export const api = createApi<AppRouter>();
|
|
225
|
-
|
|
226
|
-
// 2. Use in Server Components for SSR
|
|
227
|
-
export default async function Page() {
|
|
228
|
-
const data = await api.getData.call({}); // SSR
|
|
229
|
-
return <div>{data}</div>;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 3. Handle loading states in Client Components
|
|
233
|
-
const [loading, setLoading] = useState(false);
|
|
234
|
-
|
|
235
|
-
// 4. Use Server Actions for mutations
|
|
236
|
-
'use server';
|
|
237
|
-
export async function createItem(formData: FormData) {
|
|
238
|
-
return api.createItem.call({ body: { ... } });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// 5. Type-safe error handling
|
|
242
|
-
try {
|
|
243
|
-
await api.getUser.call({ params: { id } });
|
|
244
|
-
} catch (e) {
|
|
245
|
-
if (e.status === 404) redirect('/not-found');
|
|
246
|
-
}
|
|
247
|
-
```
|