yinzerflow 0.2.9 → 0.3.0
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/docs/core/context.md +138 -0
- package/docs/core/examples.md +259 -10
- package/docs/core/routes.md +190 -27
- package/index.d.ts +1675 -26
- package/index.js +12 -12
- package/index.js.map +9 -7
- package/package.json +1 -1
- package/example/README.md +0 -11
- package/example/app/handlers/example.ts +0 -27
- package/example/app/index.ts +0 -107
- package/example/app/routes/example.ts +0 -18
- package/example/app/routes/group-example.ts +0 -13
- package/example/app/util/customLogger.ts +0 -166
- package/example/docker-compose.yml +0 -28
- package/example/package.json +0 -16
- package/example/tsconfig.json +0 -54
package/docs/core/context.md
CHANGED
|
@@ -51,6 +51,144 @@ Contains all incoming request data including headers, body, query parameters, ro
|
|
|
51
51
|
### Response Object (`ctx.response`)
|
|
52
52
|
Provides methods to control the HTTP response including status codes, headers, and response formatting. See [Response Object Documentation](./response.md) for detailed information about response methods and capabilities.
|
|
53
53
|
|
|
54
|
+
## Context State
|
|
55
|
+
|
|
56
|
+
The Context object provides a powerful state system that allows you to store and share custom data throughout the request lifecycle. This is perfect for authentication, middleware data, request-scoped variables, and custom context information.
|
|
57
|
+
|
|
58
|
+
### What is Context State?
|
|
59
|
+
|
|
60
|
+
Context state is a request-scoped object (`ctx.state`) that persists data throughout the entire request lifecycle. Unlike global variables, state is isolated to each individual request and automatically garbage collected when the request completes.
|
|
61
|
+
|
|
62
|
+
### Basic Usage
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
app.get('/api/users', async (ctx) => {
|
|
66
|
+
// Store custom data in state
|
|
67
|
+
ctx.state.user = { id: 1, name: 'John' };
|
|
68
|
+
ctx.state.requestId = generateRequestId();
|
|
69
|
+
ctx.state.timestamp = Date.now();
|
|
70
|
+
|
|
71
|
+
// Access the data later
|
|
72
|
+
console.log(ctx.state.user.name); // "John"
|
|
73
|
+
console.log(ctx.state.requestId); // "req-123"
|
|
74
|
+
console.log(ctx.state.timestamp); // 1703123456789
|
|
75
|
+
|
|
76
|
+
return { users: ['John', 'Jane'] };
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Advanced Usage with Type Safety
|
|
81
|
+
|
|
82
|
+
For full type safety, you can extend the context with custom state types:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Define your custom state interface
|
|
86
|
+
interface AuthContext extends InternalHandlerCallbackGenerics {
|
|
87
|
+
state: {
|
|
88
|
+
user: User;
|
|
89
|
+
permissions: string[];
|
|
90
|
+
requestId: string;
|
|
91
|
+
session: Session;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Use it in your route handler
|
|
96
|
+
const authHandler: HandlerCallback<AuthContext> = async (ctx) => {
|
|
97
|
+
// Fully type-safe access to state
|
|
98
|
+
console.log(ctx.state.user.name); // Type: string
|
|
99
|
+
console.log(ctx.state.permissions[0]); // Type: string
|
|
100
|
+
console.log(ctx.state.session.expiresAt); // Type: Date
|
|
101
|
+
|
|
102
|
+
// No type assertions needed!
|
|
103
|
+
const user = ctx.state.user; // Type: User
|
|
104
|
+
const permissions = ctx.state.permissions; // Type: string[]
|
|
105
|
+
|
|
106
|
+
return { message: 'Authenticated', user };
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### State Lifecycle
|
|
111
|
+
|
|
112
|
+
State data follows the request lifecycle:
|
|
113
|
+
|
|
114
|
+
1. **Request Start**: State object is created as empty object
|
|
115
|
+
2. **Global Hooks**: `beforeAll` hooks can populate state
|
|
116
|
+
3. **Route Hooks**: `beforeHooks` can access and modify state
|
|
117
|
+
4. **Route Handler**: Your handler can access and modify state
|
|
118
|
+
5. **Route Hooks**: `afterHooks` can access state and modify response
|
|
119
|
+
6. **Global Hooks**: `afterAll` hooks can access state and modify response
|
|
120
|
+
7. **Request End**: State is automatically garbage collected
|
|
121
|
+
|
|
122
|
+
### State Inheritance in Route Groups
|
|
123
|
+
|
|
124
|
+
State can be shared across route groups and inherited by nested routes:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
app.group('/api/v1', (api) => {
|
|
128
|
+
// Group-level middleware sets state
|
|
129
|
+
api.beforeAll([async (ctx) => {
|
|
130
|
+
ctx.state.apiVersion = 'v1';
|
|
131
|
+
ctx.state.environment = process.env.NODE_ENV;
|
|
132
|
+
}]);
|
|
133
|
+
|
|
134
|
+
api.group('/admin', (admin) => {
|
|
135
|
+
// Admin-specific middleware
|
|
136
|
+
admin.beforeAll([async (ctx) => {
|
|
137
|
+
ctx.state.requiresAuth = true;
|
|
138
|
+
ctx.state.adminOnly = true;
|
|
139
|
+
}]);
|
|
140
|
+
|
|
141
|
+
// Routes inherit all state from parent groups
|
|
142
|
+
admin.get('/users', async (ctx) => {
|
|
143
|
+
console.log(ctx.state.apiVersion); // "v1"
|
|
144
|
+
console.log(ctx.state.environment); // "production"
|
|
145
|
+
console.log(ctx.state.requiresAuth); // true
|
|
146
|
+
console.log(ctx.state.adminOnly); // true
|
|
147
|
+
|
|
148
|
+
return { users: ['Admin1', 'Admin2'] };
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Common Use Cases
|
|
155
|
+
|
|
156
|
+
- **Authentication**: Store user information and permissions
|
|
157
|
+
- **Request Tracking**: Generate and store request IDs for logging
|
|
158
|
+
- **Middleware Data**: Pass data between middleware and route handlers
|
|
159
|
+
- **Session Management**: Store session information and user preferences
|
|
160
|
+
- **Rate Limiting**: Track request counts and limits per user/IP
|
|
161
|
+
- **Custom Headers**: Store computed headers for later use
|
|
162
|
+
- **Validation Results**: Cache validation results to avoid re-validation
|
|
163
|
+
- **Database Connections**: Store database connection pools or transactions
|
|
164
|
+
|
|
165
|
+
### Best Practices
|
|
166
|
+
|
|
167
|
+
- **Keep state minimal**: Only store data that's actually needed
|
|
168
|
+
- **Use descriptive keys**: `ctx.state.user` is better than `ctx.state.u`
|
|
169
|
+
- **Validate state access**: Check if properties exist before using them
|
|
170
|
+
- **Type your state**: Use TypeScript interfaces for complex state structures
|
|
171
|
+
- **Don't store sensitive data**: State is not encrypted or secured
|
|
172
|
+
- **Clean up resources**: Release any resources (like DB connections) when done
|
|
173
|
+
|
|
174
|
+
### Security Considerations
|
|
175
|
+
|
|
176
|
+
YinzerFlow implements several security measures for context state:
|
|
177
|
+
|
|
178
|
+
#### 🛡️ Request Isolation
|
|
179
|
+
- **Problem**: State data could leak between requests
|
|
180
|
+
- **YinzerFlow Solution**: Each request gets its own isolated state object
|
|
181
|
+
|
|
182
|
+
#### 🛡️ Type Safety
|
|
183
|
+
- **Problem**: Unchecked state access can lead to runtime errors
|
|
184
|
+
- **YinzerFlow Solution**: TypeScript generics provide compile-time type checking
|
|
185
|
+
|
|
186
|
+
#### 🛡️ Memory Management
|
|
187
|
+
- **Problem**: State data could accumulate and cause memory leaks
|
|
188
|
+
- **YinzerFlow Solution**: State is automatically garbage collected after each request
|
|
189
|
+
|
|
190
|
+
These security measures ensure YinzerFlow's context state implementation follows security best practices and prevents common attack vectors while maintaining spec compliance.
|
|
191
|
+
|
|
54
192
|
## TypeScript Support
|
|
55
193
|
|
|
56
194
|
YinzerFlow provides full TypeScript support for context objects with generic type parameters.
|
package/docs/core/examples.md
CHANGED
|
@@ -97,20 +97,269 @@ app.get('/api/users/:id', (ctx) => {
|
|
|
97
97
|
|
|
98
98
|
## Error Handler Pattern
|
|
99
99
|
|
|
100
|
+
## Context State Examples
|
|
101
|
+
|
|
102
|
+
### Basic State Usage
|
|
103
|
+
|
|
100
104
|
```typescript
|
|
101
|
-
app.
|
|
102
|
-
//
|
|
103
|
-
|
|
105
|
+
app.get('/api/users', async (ctx) => {
|
|
106
|
+
// Store simple data in state
|
|
107
|
+
ctx.state.requestId = generateRequestId();
|
|
108
|
+
ctx.state.timestamp = Date.now();
|
|
109
|
+
ctx.state.clientIp = ctx.request.ipAddress;
|
|
104
110
|
|
|
105
|
-
//
|
|
106
|
-
ctx.
|
|
111
|
+
// Access the stored data
|
|
112
|
+
console.log(`Request ${ctx.state.requestId} from ${ctx.state.clientIp}`);
|
|
113
|
+
|
|
114
|
+
return { users: ['John', 'Jane'] };
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Authentication State Pattern
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Authentication middleware
|
|
122
|
+
const authMiddleware: HandlerCallback = async (ctx) => {
|
|
123
|
+
const token = ctx.request.headers.authorization?.replace('Bearer ', '');
|
|
124
|
+
|
|
125
|
+
if (!token) {
|
|
126
|
+
throw new Error('Unauthorized');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const user = await validateJWT(token);
|
|
131
|
+
const permissions = await getUserPermissions(user.id);
|
|
132
|
+
|
|
133
|
+
// Store authentication data in state
|
|
134
|
+
ctx.state.user = user;
|
|
135
|
+
ctx.state.permissions = permissions;
|
|
136
|
+
ctx.state.isAuthenticated = true;
|
|
137
|
+
ctx.state.authTimestamp = Date.now();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error('Invalid token');
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Protected route using the state
|
|
144
|
+
app.get('/api/admin/users', authMiddleware, async (ctx) => {
|
|
145
|
+
// Access the authenticated user data
|
|
146
|
+
const { user, permissions, isAuthenticated } = ctx.state;
|
|
147
|
+
|
|
148
|
+
if (!permissions.includes('admin')) {
|
|
149
|
+
throw new Error('Insufficient permissions');
|
|
150
|
+
}
|
|
107
151
|
|
|
108
152
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
153
|
+
message: 'Admin access granted',
|
|
154
|
+
user: { id: user.id, name: user.name },
|
|
155
|
+
permissions,
|
|
156
|
+
authenticatedAt: ctx.state.authTimestamp
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Typed State Pattern
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Define your state interface
|
|
165
|
+
interface UserContext extends InternalHandlerCallbackGenerics {
|
|
166
|
+
state: {
|
|
167
|
+
user: User;
|
|
168
|
+
permissions: string[];
|
|
169
|
+
requestId: string;
|
|
170
|
+
session: {
|
|
171
|
+
id: string;
|
|
172
|
+
expiresAt: Date;
|
|
173
|
+
lastActivity: Date;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Use typed state in your handler
|
|
179
|
+
const userProfileHandler: HandlerCallback<UserContext> = async (ctx) => {
|
|
180
|
+
// Fully type-safe access to state
|
|
181
|
+
const { user, permissions, session } = ctx.state;
|
|
182
|
+
|
|
183
|
+
// No type assertions needed!
|
|
184
|
+
if (session.expiresAt < new Date()) {
|
|
185
|
+
throw new Error('Session expired');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Update session activity
|
|
189
|
+
ctx.state.session.lastActivity = new Date();
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
profile: {
|
|
193
|
+
id: user.id,
|
|
194
|
+
name: user.name,
|
|
195
|
+
email: user.email,
|
|
196
|
+
permissions
|
|
197
|
+
},
|
|
198
|
+
session: {
|
|
199
|
+
id: session.id,
|
|
200
|
+
expiresAt: session.expiresAt,
|
|
201
|
+
lastActivity: ctx.state.session.lastActivity
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Middleware Chain State Pattern
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Rate limiting middleware
|
|
211
|
+
const rateLimitMiddleware: HandlerCallback = async (ctx) => {
|
|
212
|
+
const clientIp = ctx.request.ipAddress;
|
|
213
|
+
const currentCount = await getRequestCount(clientIp);
|
|
214
|
+
|
|
215
|
+
if (currentCount > 100) {
|
|
216
|
+
throw new Error('Rate limit exceeded');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Store rate limiting data in state
|
|
220
|
+
ctx.state.rateLimit = {
|
|
221
|
+
clientIp,
|
|
222
|
+
currentCount,
|
|
223
|
+
limit: 100,
|
|
224
|
+
resetTime: Date.now() + 60000 // 1 minute
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await incrementRequestCount(clientIp);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Logging middleware
|
|
231
|
+
const loggingMiddleware: HandlerCallback = async (ctx) => {
|
|
232
|
+
// Access state from previous middleware
|
|
233
|
+
const rateLimit = ctx.state.rateLimit;
|
|
234
|
+
|
|
235
|
+
ctx.state.logData = {
|
|
236
|
+
requestId: generateRequestId(),
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
clientIp: ctx.request.ipAddress,
|
|
239
|
+
rateLimitInfo: rateLimit,
|
|
240
|
+
userAgent: ctx.request.headers['user-agent']
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
console.log('Request started:', ctx.state.logData);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Route using both middlewares
|
|
247
|
+
app.get('/api/data', rateLimitMiddleware, loggingMiddleware, async (ctx) => {
|
|
248
|
+
// Access state from all previous middleware
|
|
249
|
+
const { rateLimit, logData } = ctx.state;
|
|
250
|
+
|
|
251
|
+
// Add route-specific data to state
|
|
252
|
+
ctx.state.routeData = {
|
|
253
|
+
endpoint: '/api/data',
|
|
254
|
+
method: 'GET',
|
|
255
|
+
processingTime: Date.now() - logData.timestamp
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
message: 'Data retrieved successfully',
|
|
260
|
+
rateLimit: {
|
|
261
|
+
remaining: rateLimit.limit - rateLimit.currentCount,
|
|
262
|
+
resetTime: rateLimit.resetTime
|
|
263
|
+
},
|
|
264
|
+
requestInfo: logData,
|
|
265
|
+
routeInfo: ctx.state.routeData
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Route Group State Pattern
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
app.group('/api/v1', (api) => {
|
|
274
|
+
// Global API state
|
|
275
|
+
api.beforeAll([async (ctx) => {
|
|
276
|
+
ctx.state.apiVersion = 'v1';
|
|
277
|
+
ctx.state.environment = process.env.NODE_ENV;
|
|
278
|
+
ctx.state.baseUrl = 'https://api.example.com';
|
|
279
|
+
}]);
|
|
280
|
+
|
|
281
|
+
api.group('/admin', (admin) => {
|
|
282
|
+
// Admin-specific state
|
|
283
|
+
admin.beforeAll([async (ctx) => {
|
|
284
|
+
ctx.state.requiresAuth = true;
|
|
285
|
+
ctx.state.adminOnly = true;
|
|
286
|
+
ctx.state.auditLog = true;
|
|
287
|
+
}]);
|
|
288
|
+
|
|
289
|
+
admin.group('/users', (users) => {
|
|
290
|
+
// User management state
|
|
291
|
+
users.beforeAll([async (ctx) => {
|
|
292
|
+
ctx.state.resourceType = 'user';
|
|
293
|
+
ctx.state.allowedOperations = ['create', 'read', 'update', 'delete'];
|
|
294
|
+
}]);
|
|
295
|
+
|
|
296
|
+
// Route inherits all state from parent groups
|
|
297
|
+
users.get('/', async (ctx) => {
|
|
298
|
+
const {
|
|
299
|
+
apiVersion, // "v1"
|
|
300
|
+
environment, // "production"
|
|
301
|
+
requiresAuth, // true
|
|
302
|
+
adminOnly, // true
|
|
303
|
+
auditLog, // true
|
|
304
|
+
resourceType, // "user"
|
|
305
|
+
allowedOperations // ["create", "read", "update", "delete"]
|
|
306
|
+
} = ctx.state;
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
message: 'User list retrieved',
|
|
310
|
+
apiInfo: { version: apiVersion, environment },
|
|
311
|
+
security: { requiresAuth, adminOnly, auditLog },
|
|
312
|
+
resource: { type: resourceType, operations: allowedOperations }
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### State Validation Pattern
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// State validation helper
|
|
324
|
+
const validateState = (ctx: Context, requiredKeys: string[]) => {
|
|
325
|
+
const missing = requiredKeys.filter(key => !(key in ctx.state));
|
|
326
|
+
|
|
327
|
+
if (missing.length > 0) {
|
|
328
|
+
throw new Error(`Missing required state: ${missing.join(', ')}`);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Middleware that sets required state
|
|
333
|
+
const userContextMiddleware: HandlerCallback = async (ctx) => {
|
|
334
|
+
const userId = ctx.request.params.userId;
|
|
335
|
+
|
|
336
|
+
if (!userId) {
|
|
337
|
+
throw new Error('User ID required');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const user = await getUserById(userId);
|
|
341
|
+
if (!user) {
|
|
342
|
+
throw new Error('User not found');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Set required state
|
|
346
|
+
ctx.state.user = user;
|
|
347
|
+
ctx.state.userId = userId;
|
|
348
|
+
ctx.state.userPermissions = await getUserPermissions(userId);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Route that validates state
|
|
352
|
+
app.get('/api/users/:userId/profile', userContextMiddleware, async (ctx) => {
|
|
353
|
+
// Validate required state
|
|
354
|
+
validateState(ctx, ['user', 'userId', 'userPermissions']);
|
|
355
|
+
|
|
356
|
+
// Now we can safely access state
|
|
357
|
+
const { user, userPermissions } = ctx.state;
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
profile: user,
|
|
361
|
+
permissions: userPermissions,
|
|
362
|
+
lastAccessed: new Date().toISOString()
|
|
114
363
|
};
|
|
115
364
|
});
|
|
116
365
|
```
|
package/docs/core/routes.md
CHANGED
|
@@ -144,6 +144,83 @@ app.get('/users/:userId/posts/:postId', ({ request }) => {
|
|
|
144
144
|
- Use consistent naming conventions
|
|
145
145
|
- Avoid generic names that could cause confusion
|
|
146
146
|
|
|
147
|
+
## Context State
|
|
148
|
+
|
|
149
|
+
YinzerFlow provides a powerful context state system that allows you to store and share custom data throughout the request lifecycle. This is perfect for authentication, middleware data, request-scoped variables, and custom context information.
|
|
150
|
+
|
|
151
|
+
### Quick Reference
|
|
152
|
+
|
|
153
|
+
The context state is accessible via `ctx.state` and can store any data you need:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
app.get('/api/users', async (ctx) => {
|
|
157
|
+
// Store custom data in state
|
|
158
|
+
ctx.state.user = { id: 1, name: 'John' };
|
|
159
|
+
ctx.state.requestId = generateRequestId();
|
|
160
|
+
|
|
161
|
+
// Access the data later
|
|
162
|
+
console.log(ctx.state.user.name); // "John"
|
|
163
|
+
console.log(ctx.state.requestId); // "req-123"
|
|
164
|
+
|
|
165
|
+
return { users: ['John', 'Jane'] };
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Middleware Integration
|
|
170
|
+
|
|
171
|
+
State is perfect for middleware that needs to pass data to route handlers:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Authentication middleware
|
|
175
|
+
const authMiddleware: HandlerCallback = async (ctx) => {
|
|
176
|
+
const token = ctx.request.headers.authorization;
|
|
177
|
+
const user = await validateToken(token);
|
|
178
|
+
|
|
179
|
+
// Store user data in state for route handlers
|
|
180
|
+
ctx.state.user = user;
|
|
181
|
+
ctx.state.permissions = await getUserPermissions(user.id);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Route that uses the authenticated state
|
|
185
|
+
app.get('/api/admin/users', authMiddleware, async (ctx) => {
|
|
186
|
+
const { user, permissions } = ctx.state;
|
|
187
|
+
|
|
188
|
+
if (!permissions.includes('admin')) {
|
|
189
|
+
throw new Error('Insufficient permissions');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { message: 'Admin access granted', user };
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Route Group State
|
|
197
|
+
|
|
198
|
+
State can be shared across route groups and inherited by nested routes:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
app.group('/api/v1', (api) => {
|
|
202
|
+
api.beforeAll([async (ctx) => {
|
|
203
|
+
ctx.state.apiVersion = 'v1';
|
|
204
|
+
ctx.state.environment = process.env.NODE_ENV;
|
|
205
|
+
}]);
|
|
206
|
+
|
|
207
|
+
api.group('/admin', (admin) => {
|
|
208
|
+
admin.beforeAll([async (ctx) => {
|
|
209
|
+
ctx.state.requiresAuth = true;
|
|
210
|
+
ctx.state.adminOnly = true;
|
|
211
|
+
}]);
|
|
212
|
+
|
|
213
|
+
// Routes inherit all state from parent groups
|
|
214
|
+
admin.get('/users', async (ctx) => {
|
|
215
|
+
const { apiVersion, environment, requiresAuth, adminOnly } = ctx.state;
|
|
216
|
+
return { users: ['Admin1', 'Admin2'] };
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**📚 For comprehensive Context State documentation including TypeScript patterns, security considerations, and best practices, see [Context State Documentation](./context.md#context-state).**
|
|
223
|
+
|
|
147
224
|
## Query Parameters
|
|
148
225
|
|
|
149
226
|
URL query strings are automatically parsed and available in `ctx.request.query`:
|
|
@@ -305,10 +382,11 @@ Global hooks are executed in the following order:
|
|
|
305
382
|
|
|
306
383
|
## Route Groups
|
|
307
384
|
|
|
308
|
-
|
|
385
|
+
Route groups allow you to organize related routes under a common prefix and apply shared middleware or hooks to all routes within the group. Groups can be nested to create hierarchical route structures.
|
|
386
|
+
|
|
387
|
+
### Basic Group Usage
|
|
309
388
|
|
|
310
389
|
```typescript
|
|
311
|
-
// API v1 routes with authentication
|
|
312
390
|
app.group('/api/v1', (group) => {
|
|
313
391
|
group.get('/users', ({ response }) => {
|
|
314
392
|
return { users: ['John', 'Jane'] };
|
|
@@ -331,49 +409,134 @@ app.group('/api/v1', (group) => {
|
|
|
331
409
|
}
|
|
332
410
|
]
|
|
333
411
|
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Nested Groups
|
|
334
415
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
416
|
+
Groups can be nested to create hierarchical route structures like `/api/v1/admin/users`:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
app.group('/api/v1', (api) => {
|
|
420
|
+
// /api/v1/users
|
|
421
|
+
api.group('/users', (users) => {
|
|
422
|
+
users.get('/', ({ response }) => {
|
|
423
|
+
return { users: ['John', 'Jane'] };
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
users.get('/:id', ({ request }) => {
|
|
427
|
+
const userId = request.params.id;
|
|
428
|
+
return { userId, name: 'John Doe' };
|
|
429
|
+
});
|
|
339
430
|
});
|
|
340
431
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
432
|
+
// /api/v1/admin
|
|
433
|
+
api.group('/admin', (admin) => {
|
|
434
|
+
// /api/v1/admin/users
|
|
435
|
+
admin.group('/users', (adminUsers) => {
|
|
436
|
+
adminUsers.get('/', ({ response }) => {
|
|
437
|
+
return { adminUsers: ['Admin1', 'Admin2'] };
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
adminUsers.post('/', ({ request }) => {
|
|
441
|
+
const userData = request.body;
|
|
442
|
+
return { message: 'Admin user created', data: userData };
|
|
443
|
+
});
|
|
444
|
+
}, {
|
|
445
|
+
beforeHooks: [
|
|
446
|
+
({ request }) => {
|
|
447
|
+
// Admin authentication check
|
|
448
|
+
if (!request.headers.authorization) {
|
|
449
|
+
throw new Error('Unauthorized');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
});
|
|
454
|
+
}, {
|
|
455
|
+
beforeHooks: [
|
|
456
|
+
({ request }) => {
|
|
457
|
+
// Admin role verification
|
|
458
|
+
console.log('Admin route accessed');
|
|
459
|
+
}
|
|
460
|
+
]
|
|
344
461
|
});
|
|
345
462
|
}, {
|
|
346
463
|
beforeHooks: [
|
|
347
|
-
({ request
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
if (!adminToken) {
|
|
351
|
-
response.setStatusCode(403);
|
|
352
|
-
return { error: 'Admin access required' };
|
|
353
|
-
}
|
|
464
|
+
({ request }) => {
|
|
465
|
+
// Global API logging
|
|
466
|
+
console.log(`API request to: ${request.url}`);
|
|
354
467
|
}
|
|
355
468
|
]
|
|
356
469
|
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Hook Inheritance
|
|
473
|
+
|
|
474
|
+
Hooks are inherited and merged from parent groups to child groups:
|
|
357
475
|
|
|
358
|
-
|
|
359
|
-
app.group('/api
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
476
|
+
```typescript
|
|
477
|
+
app.group('/api', (api) => {
|
|
478
|
+
// This group inherits the 'api' beforeHooks
|
|
479
|
+
|
|
480
|
+
api.group('/v1', (v1) => {
|
|
481
|
+
// This group inherits both 'api' and 'v1' beforeHooks
|
|
482
|
+
|
|
483
|
+
v1.group('/admin', (admin) => {
|
|
484
|
+
// This group inherits 'api', 'v1', and 'admin' beforeHooks
|
|
485
|
+
|
|
486
|
+
admin.get('/dashboard', ({ response }) => {
|
|
487
|
+
// All three beforeHooks will execute in order
|
|
488
|
+
return { message: 'Admin dashboard' };
|
|
489
|
+
});
|
|
490
|
+
}, {
|
|
491
|
+
beforeHooks: [
|
|
492
|
+
({ request }) => {
|
|
493
|
+
// Admin-specific hook
|
|
494
|
+
console.log('Admin route accessed');
|
|
495
|
+
}
|
|
496
|
+
]
|
|
363
497
|
});
|
|
364
498
|
}, {
|
|
365
499
|
beforeHooks: [
|
|
366
|
-
() => {
|
|
367
|
-
//
|
|
368
|
-
console.log('
|
|
500
|
+
({ request }) => {
|
|
501
|
+
// Version-specific hook
|
|
502
|
+
console.log('API v1 accessed');
|
|
369
503
|
}
|
|
370
504
|
]
|
|
371
505
|
});
|
|
372
506
|
}, {
|
|
373
507
|
beforeHooks: [
|
|
374
|
-
() => {
|
|
375
|
-
// API
|
|
376
|
-
console.log('API
|
|
508
|
+
({ request }) => {
|
|
509
|
+
// Global API hook
|
|
510
|
+
console.log('API request');
|
|
511
|
+
}
|
|
512
|
+
]
|
|
513
|
+
});
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Group Options
|
|
517
|
+
|
|
518
|
+
Groups support the same options as individual routes:
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
app.group('/api/v1', (group) => {
|
|
522
|
+
group.get('/public', ({ response }) => {
|
|
523
|
+
return { message: 'Public endpoint' };
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
group.get('/private', ({ response }) => {
|
|
527
|
+
return { message: 'Private endpoint' };
|
|
528
|
+
});
|
|
529
|
+
}, {
|
|
530
|
+
beforeHooks: [
|
|
531
|
+
({ request }) => {
|
|
532
|
+
// Global group hooks
|
|
533
|
+
console.log('API v1 request');
|
|
534
|
+
}
|
|
535
|
+
],
|
|
536
|
+
afterHooks: [
|
|
537
|
+
({ response }) => {
|
|
538
|
+
// Global group after hooks
|
|
539
|
+
response.headers.set('X-API-Version', 'v1');
|
|
377
540
|
}
|
|
378
541
|
]
|
|
379
542
|
});
|