@ranimontagna/agent-toolkit 0.1.4 → 0.1.5
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 +282 -277
- package/docs/assets/install-plan.svg +29 -0
- package/docs/assets/install-skill-packages.svg +31 -0
- package/docs/assets/install-status.svg +32 -0
- package/package.json +10 -9
- package/setup-agent-toolkit.sh +1 -1
- package/skills/backend/fastify-best-practices/LICENSE +21 -0
- package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
- package/skills/backend/fastify-best-practices/SKILL.md +75 -0
- package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
- package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
- package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
- package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
- package/skills/backend/fastify-best-practices/rules/database.md +320 -0
- package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
- package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
- package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
- package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
- package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
- package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
- package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
- package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
- package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
- package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
- package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
- package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
- package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
- package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
- package/skills/backend/fastify-best-practices/tile.json +11 -0
- package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hooks
|
|
3
|
+
description: Hooks and request lifecycle in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: hooks, lifecycle, middleware, onRequest, preHandler
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Hooks and Request Lifecycle
|
|
9
|
+
|
|
10
|
+
## Request Lifecycle Overview
|
|
11
|
+
|
|
12
|
+
Fastify executes hooks in a specific order:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Incoming Request
|
|
16
|
+
|
|
|
17
|
+
onRequest
|
|
18
|
+
|
|
|
19
|
+
preParsing
|
|
20
|
+
|
|
|
21
|
+
preValidation
|
|
22
|
+
|
|
|
23
|
+
preHandler
|
|
24
|
+
|
|
|
25
|
+
Handler
|
|
26
|
+
|
|
|
27
|
+
preSerialization
|
|
28
|
+
|
|
|
29
|
+
onSend
|
|
30
|
+
|
|
|
31
|
+
onResponse
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## onRequest Hook
|
|
35
|
+
|
|
36
|
+
First hook to execute, before body parsing. Use for authentication, request ID setup:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import Fastify from 'fastify';
|
|
40
|
+
|
|
41
|
+
const app = Fastify();
|
|
42
|
+
|
|
43
|
+
// Global onRequest hook
|
|
44
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
45
|
+
request.startTime = Date.now();
|
|
46
|
+
request.log.info({ url: request.url, method: request.method }, 'Request started');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Authentication check
|
|
50
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
51
|
+
// Skip auth for public routes
|
|
52
|
+
if (request.url.startsWith('/public')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
57
|
+
if (!token) {
|
|
58
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
59
|
+
return; // Stop processing
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
request.user = await verifyToken(token);
|
|
64
|
+
} catch {
|
|
65
|
+
reply.code(401).send({ error: 'Invalid token' });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## preParsing Hook
|
|
71
|
+
|
|
72
|
+
Execute before body parsing. Can modify the payload stream:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
app.addHook('preParsing', async (request, reply, payload) => {
|
|
76
|
+
// Log raw payload size
|
|
77
|
+
request.log.debug({ contentLength: request.headers['content-length'] }, 'Parsing body');
|
|
78
|
+
|
|
79
|
+
// Return modified payload stream if needed
|
|
80
|
+
return payload;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Decompress incoming data
|
|
84
|
+
app.addHook('preParsing', async (request, reply, payload) => {
|
|
85
|
+
if (request.headers['content-encoding'] === 'gzip') {
|
|
86
|
+
return payload.pipe(zlib.createGunzip());
|
|
87
|
+
}
|
|
88
|
+
return payload;
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## preValidation Hook
|
|
93
|
+
|
|
94
|
+
Execute after parsing, before schema validation:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
app.addHook('preValidation', async (request, reply) => {
|
|
98
|
+
// Modify body before validation
|
|
99
|
+
if (request.body && typeof request.body === 'object') {
|
|
100
|
+
// Normalize data
|
|
101
|
+
request.body.email = request.body.email?.toLowerCase().trim();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Rate limiting check
|
|
106
|
+
app.addHook('preValidation', async (request, reply) => {
|
|
107
|
+
const key = request.ip;
|
|
108
|
+
const count = await redis.incr(`ratelimit:${key}`);
|
|
109
|
+
|
|
110
|
+
if (count === 1) {
|
|
111
|
+
await redis.expire(`ratelimit:${key}`, 60);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (count > 100) {
|
|
115
|
+
reply.code(429).send({ error: 'Too many requests' });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## preHandler Hook
|
|
121
|
+
|
|
122
|
+
Most common hook, execute after validation, before handler:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Authorization check
|
|
126
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
127
|
+
const { userId } = request.params as { userId: string };
|
|
128
|
+
|
|
129
|
+
if (request.user.id !== userId && !request.user.isAdmin) {
|
|
130
|
+
reply.code(403).send({ error: 'Forbidden' });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Load related data
|
|
135
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
136
|
+
if (request.params?.projectId) {
|
|
137
|
+
request.project = await db.projects.findById(request.params.projectId);
|
|
138
|
+
if (!request.project) {
|
|
139
|
+
reply.code(404).send({ error: 'Project not found' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Transaction wrapper
|
|
145
|
+
app.addHook('preHandler', async (request) => {
|
|
146
|
+
request.transaction = await db.beginTransaction();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.addHook('onResponse', async (request) => {
|
|
150
|
+
if (request.transaction) {
|
|
151
|
+
await request.transaction.commit();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
app.addHook('onError', async (request, reply, error) => {
|
|
156
|
+
if (request.transaction) {
|
|
157
|
+
await request.transaction.rollback();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## preSerialization Hook
|
|
163
|
+
|
|
164
|
+
Modify payload before serialization:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
app.addHook('preSerialization', async (request, reply, payload) => {
|
|
168
|
+
// Add metadata to all responses
|
|
169
|
+
if (payload && typeof payload === 'object') {
|
|
170
|
+
return {
|
|
171
|
+
...payload,
|
|
172
|
+
_meta: {
|
|
173
|
+
requestId: request.id,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return payload;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Remove sensitive fields
|
|
182
|
+
app.addHook('preSerialization', async (request, reply, payload) => {
|
|
183
|
+
if (payload?.user?.password) {
|
|
184
|
+
const { password, ...user } = payload.user;
|
|
185
|
+
return { ...payload, user };
|
|
186
|
+
}
|
|
187
|
+
return payload;
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## onSend Hook
|
|
192
|
+
|
|
193
|
+
Modify response after serialization:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
app.addHook('onSend', async (request, reply, payload) => {
|
|
197
|
+
// Add response headers
|
|
198
|
+
reply.header('X-Response-Time', Date.now() - request.startTime);
|
|
199
|
+
|
|
200
|
+
// Compress response
|
|
201
|
+
if (payload && payload.length > 1024) {
|
|
202
|
+
const compressed = await gzip(payload);
|
|
203
|
+
reply.header('Content-Encoding', 'gzip');
|
|
204
|
+
return compressed;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return payload;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Transform JSON string response
|
|
211
|
+
app.addHook('onSend', async (request, reply, payload) => {
|
|
212
|
+
if (reply.getHeader('content-type')?.includes('application/json')) {
|
|
213
|
+
// payload is already a string at this point
|
|
214
|
+
return payload;
|
|
215
|
+
}
|
|
216
|
+
return payload;
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## onResponse Hook
|
|
221
|
+
|
|
222
|
+
Execute after response is sent. Cannot modify response:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
app.addHook('onResponse', async (request, reply) => {
|
|
226
|
+
// Log response time
|
|
227
|
+
const responseTime = Date.now() - request.startTime;
|
|
228
|
+
request.log.info({
|
|
229
|
+
method: request.method,
|
|
230
|
+
url: request.url,
|
|
231
|
+
statusCode: reply.statusCode,
|
|
232
|
+
responseTime,
|
|
233
|
+
}, 'Request completed');
|
|
234
|
+
|
|
235
|
+
// Track metrics
|
|
236
|
+
metrics.histogram('http_request_duration', responseTime, {
|
|
237
|
+
method: request.method,
|
|
238
|
+
route: request.routeOptions.url,
|
|
239
|
+
status: reply.statusCode,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## onError Hook
|
|
245
|
+
|
|
246
|
+
Execute when an error is thrown:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
app.addHook('onError', async (request, reply, error) => {
|
|
250
|
+
// Log error details
|
|
251
|
+
request.log.error({
|
|
252
|
+
err: error,
|
|
253
|
+
url: request.url,
|
|
254
|
+
method: request.method,
|
|
255
|
+
body: request.body,
|
|
256
|
+
}, 'Request error');
|
|
257
|
+
|
|
258
|
+
// Track error metrics
|
|
259
|
+
metrics.increment('http_errors', {
|
|
260
|
+
error: error.code || 'UNKNOWN',
|
|
261
|
+
route: request.routeOptions.url,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Cleanup resources
|
|
265
|
+
if (request.tempFile) {
|
|
266
|
+
await fs.unlink(request.tempFile).catch(() => {});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## onTimeout Hook
|
|
272
|
+
|
|
273
|
+
Execute when request times out:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const app = Fastify({
|
|
277
|
+
connectionTimeout: 30000, // 30 seconds
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
app.addHook('onTimeout', async (request, reply) => {
|
|
281
|
+
request.log.warn({
|
|
282
|
+
url: request.url,
|
|
283
|
+
method: request.method,
|
|
284
|
+
}, 'Request timeout');
|
|
285
|
+
|
|
286
|
+
// Cleanup
|
|
287
|
+
if (request.abortController) {
|
|
288
|
+
request.abortController.abort();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## onRequestAbort Hook
|
|
294
|
+
|
|
295
|
+
Execute when client closes connection:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
app.addHook('onRequestAbort', async (request) => {
|
|
299
|
+
request.log.info('Client aborted request');
|
|
300
|
+
|
|
301
|
+
// Cancel ongoing operations
|
|
302
|
+
if (request.abortController) {
|
|
303
|
+
request.abortController.abort();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Cleanup uploaded files
|
|
307
|
+
if (request.uploadedFiles) {
|
|
308
|
+
for (const file of request.uploadedFiles) {
|
|
309
|
+
await fs.unlink(file.path).catch(() => {});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Application Lifecycle Hooks
|
|
316
|
+
|
|
317
|
+
Hooks that run at application startup/shutdown:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// After all plugins are loaded
|
|
321
|
+
app.addHook('onReady', async function () {
|
|
322
|
+
this.log.info('Server is ready');
|
|
323
|
+
|
|
324
|
+
// Initialize connections
|
|
325
|
+
await this.db.connect();
|
|
326
|
+
await this.redis.connect();
|
|
327
|
+
|
|
328
|
+
// Warm caches
|
|
329
|
+
await this.cache.warmup();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// When server is closing
|
|
333
|
+
app.addHook('onClose', async function () {
|
|
334
|
+
this.log.info('Server is closing');
|
|
335
|
+
|
|
336
|
+
// Cleanup connections
|
|
337
|
+
await this.db.close();
|
|
338
|
+
await this.redis.disconnect();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// After routes are registered
|
|
342
|
+
app.addHook('onRoute', (routeOptions) => {
|
|
343
|
+
console.log(`Route registered: ${routeOptions.method} ${routeOptions.url}`);
|
|
344
|
+
|
|
345
|
+
// Track all routes
|
|
346
|
+
routes.push({
|
|
347
|
+
method: routeOptions.method,
|
|
348
|
+
url: routeOptions.url,
|
|
349
|
+
schema: routeOptions.schema,
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// After plugin is registered
|
|
354
|
+
app.addHook('onRegister', (instance, options) => {
|
|
355
|
+
console.log(`Plugin registered with prefix: ${options.prefix}`);
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Scoped Hooks
|
|
360
|
+
|
|
361
|
+
Hooks are scoped to their encapsulation context:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
app.addHook('onRequest', async (request) => {
|
|
365
|
+
// Runs for ALL routes
|
|
366
|
+
request.log.info('Global hook');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
app.register(async function adminRoutes(fastify) {
|
|
370
|
+
// Only runs for routes in this plugin
|
|
371
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
372
|
+
if (!request.user?.isAdmin) {
|
|
373
|
+
reply.code(403).send({ error: 'Admin only' });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
fastify.get('/admin/users', async () => {
|
|
378
|
+
return { users: [] };
|
|
379
|
+
});
|
|
380
|
+
}, { prefix: '/admin' });
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Hook Execution Order
|
|
384
|
+
|
|
385
|
+
Multiple hooks of the same type execute in registration order:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
app.addHook('onRequest', async () => {
|
|
389
|
+
console.log('First');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
app.addHook('onRequest', async () => {
|
|
393
|
+
console.log('Second');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
app.addHook('onRequest', async () => {
|
|
397
|
+
console.log('Third');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Output: First, Second, Third
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Stopping Hook Execution
|
|
404
|
+
|
|
405
|
+
Return early from hooks to stop processing:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
409
|
+
if (!request.user) {
|
|
410
|
+
// Send response and return to stop further processing
|
|
411
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Continue to next hook and handler
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Route-Level Hooks
|
|
419
|
+
|
|
420
|
+
Add hooks to specific routes:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
const adminOnlyHook = async (request, reply) => {
|
|
424
|
+
if (!request.user?.isAdmin) {
|
|
425
|
+
reply.code(403).send({ error: 'Forbidden' });
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
app.get('/admin/settings', {
|
|
430
|
+
preHandler: [adminOnlyHook],
|
|
431
|
+
handler: async (request) => {
|
|
432
|
+
return { settings: {} };
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Multiple hooks
|
|
437
|
+
app.post('/orders', {
|
|
438
|
+
preValidation: [validateApiKey],
|
|
439
|
+
preHandler: [loadUser, checkQuota, logOrder],
|
|
440
|
+
handler: createOrderHandler,
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Async Hook Patterns
|
|
445
|
+
|
|
446
|
+
Always use async/await in hooks:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// GOOD - async hook
|
|
450
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
451
|
+
const user = await loadUser(request.headers.authorization);
|
|
452
|
+
request.user = user;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// AVOID - callback style (deprecated)
|
|
456
|
+
app.addHook('preHandler', (request, reply, done) => {
|
|
457
|
+
loadUser(request.headers.authorization)
|
|
458
|
+
.then((user) => {
|
|
459
|
+
request.user = user;
|
|
460
|
+
done();
|
|
461
|
+
})
|
|
462
|
+
.catch(done);
|
|
463
|
+
});
|
|
464
|
+
```
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: http-proxy
|
|
3
|
+
description: HTTP proxying and reply.from() in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: proxy, gateway, reverse-proxy, microservices
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# HTTP Proxy and Reply.from()
|
|
9
|
+
|
|
10
|
+
## @fastify/http-proxy
|
|
11
|
+
|
|
12
|
+
Use `@fastify/http-proxy` for simple reverse proxy scenarios:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import httpProxy from '@fastify/http-proxy';
|
|
17
|
+
|
|
18
|
+
const app = Fastify({ logger: true });
|
|
19
|
+
|
|
20
|
+
// Proxy all requests to /api/* to another service
|
|
21
|
+
app.register(httpProxy, {
|
|
22
|
+
upstream: 'http://backend-service:3001',
|
|
23
|
+
prefix: '/api',
|
|
24
|
+
rewritePrefix: '/v1',
|
|
25
|
+
http2: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// With authentication
|
|
29
|
+
app.register(httpProxy, {
|
|
30
|
+
upstream: 'http://internal-api:3002',
|
|
31
|
+
prefix: '/internal',
|
|
32
|
+
preHandler: async (request, reply) => {
|
|
33
|
+
// Verify authentication before proxying
|
|
34
|
+
if (!request.headers.authorization) {
|
|
35
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await app.listen({ port: 3000 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## @fastify/reply-from
|
|
44
|
+
|
|
45
|
+
For more control over proxying, use `@fastify/reply-from` with `reply.from()`:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import Fastify from 'fastify';
|
|
49
|
+
import replyFrom from '@fastify/reply-from';
|
|
50
|
+
|
|
51
|
+
const app = Fastify({ logger: true });
|
|
52
|
+
|
|
53
|
+
app.register(replyFrom, {
|
|
54
|
+
base: 'http://backend-service:3001',
|
|
55
|
+
http2: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Proxy with request/response manipulation
|
|
59
|
+
app.get('/users/:id', async (request, reply) => {
|
|
60
|
+
const { id } = request.params;
|
|
61
|
+
|
|
62
|
+
return reply.from(`/api/users/${id}`, {
|
|
63
|
+
// Modify request before forwarding
|
|
64
|
+
rewriteRequestHeaders: (originalReq, headers) => ({
|
|
65
|
+
...headers,
|
|
66
|
+
'x-request-id': request.id,
|
|
67
|
+
'x-forwarded-for': request.ip,
|
|
68
|
+
}),
|
|
69
|
+
// Modify response before sending
|
|
70
|
+
onResponse: (request, reply, res) => {
|
|
71
|
+
reply.header('x-proxy', 'fastify');
|
|
72
|
+
reply.send(res);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Conditional routing
|
|
78
|
+
app.all('/api/*', async (request, reply) => {
|
|
79
|
+
const upstream = selectUpstream(request);
|
|
80
|
+
|
|
81
|
+
return reply.from(request.url, {
|
|
82
|
+
base: upstream,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function selectUpstream(request) {
|
|
87
|
+
// Route to different backends based on request
|
|
88
|
+
if (request.headers['x-beta']) {
|
|
89
|
+
return 'http://beta-backend:3001';
|
|
90
|
+
}
|
|
91
|
+
return 'http://stable-backend:3001';
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API Gateway Pattern
|
|
96
|
+
|
|
97
|
+
Build an API gateway with multiple backends:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import Fastify from 'fastify';
|
|
101
|
+
import replyFrom from '@fastify/reply-from';
|
|
102
|
+
|
|
103
|
+
const app = Fastify({ logger: true });
|
|
104
|
+
|
|
105
|
+
// Configure multiple upstreams
|
|
106
|
+
const services = {
|
|
107
|
+
users: 'http://users-service:3001',
|
|
108
|
+
orders: 'http://orders-service:3002',
|
|
109
|
+
products: 'http://products-service:3003',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
app.register(replyFrom);
|
|
113
|
+
|
|
114
|
+
// Route to user service
|
|
115
|
+
app.register(async function (fastify) {
|
|
116
|
+
fastify.all('/*', async (request, reply) => {
|
|
117
|
+
return reply.from(request.url.replace('/users', ''), {
|
|
118
|
+
base: services.users,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}, { prefix: '/users' });
|
|
122
|
+
|
|
123
|
+
// Route to orders service
|
|
124
|
+
app.register(async function (fastify) {
|
|
125
|
+
fastify.all('/*', async (request, reply) => {
|
|
126
|
+
return reply.from(request.url.replace('/orders', ''), {
|
|
127
|
+
base: services.orders,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}, { prefix: '/orders' });
|
|
131
|
+
|
|
132
|
+
// Route to products service
|
|
133
|
+
app.register(async function (fastify) {
|
|
134
|
+
fastify.all('/*', async (request, reply) => {
|
|
135
|
+
return reply.from(request.url.replace('/products', ''), {
|
|
136
|
+
base: services.products,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}, { prefix: '/products' });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Request Body Handling
|
|
143
|
+
|
|
144
|
+
Handle request bodies when proxying:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
app.post('/api/data', async (request, reply) => {
|
|
148
|
+
return reply.from('/data', {
|
|
149
|
+
body: request.body,
|
|
150
|
+
contentType: request.headers['content-type'],
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Stream large bodies
|
|
155
|
+
app.post('/upload', async (request, reply) => {
|
|
156
|
+
return reply.from('/upload', {
|
|
157
|
+
body: request.raw,
|
|
158
|
+
contentType: request.headers['content-type'],
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
Handle upstream errors gracefully:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
app.register(replyFrom, {
|
|
169
|
+
base: 'http://backend:3001',
|
|
170
|
+
// Called when upstream returns an error
|
|
171
|
+
onError: (reply, error) => {
|
|
172
|
+
reply.log.error({ err: error }, 'Proxy error');
|
|
173
|
+
reply.code(502).send({
|
|
174
|
+
error: 'Bad Gateway',
|
|
175
|
+
message: 'Upstream service unavailable',
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Custom error handling per route
|
|
181
|
+
app.get('/data', async (request, reply) => {
|
|
182
|
+
try {
|
|
183
|
+
return await reply.from('/data');
|
|
184
|
+
} catch (error) {
|
|
185
|
+
request.log.error({ err: error }, 'Failed to proxy request');
|
|
186
|
+
return reply.code(503).send({
|
|
187
|
+
error: 'Service Unavailable',
|
|
188
|
+
retryAfter: 30,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## WebSocket Proxying
|
|
195
|
+
|
|
196
|
+
Proxy WebSocket connections:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import Fastify from 'fastify';
|
|
200
|
+
import httpProxy from '@fastify/http-proxy';
|
|
201
|
+
|
|
202
|
+
const app = Fastify({ logger: true });
|
|
203
|
+
|
|
204
|
+
app.register(httpProxy, {
|
|
205
|
+
upstream: 'http://ws-backend:3001',
|
|
206
|
+
prefix: '/ws',
|
|
207
|
+
websocket: true,
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Timeout Configuration
|
|
212
|
+
|
|
213
|
+
Configure proxy timeouts:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
app.register(replyFrom, {
|
|
217
|
+
base: 'http://backend:3001',
|
|
218
|
+
http: {
|
|
219
|
+
requestOptions: {
|
|
220
|
+
timeout: 30000, // 30 seconds
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Caching Proxied Responses
|
|
227
|
+
|
|
228
|
+
Add caching to proxied responses:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { createCache } from 'async-cache-dedupe';
|
|
232
|
+
|
|
233
|
+
const cache = createCache({
|
|
234
|
+
ttl: 60,
|
|
235
|
+
storage: { type: 'memory' },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
cache.define('proxyGet', async (url: string) => {
|
|
239
|
+
const response = await fetch(`http://backend:3001${url}`);
|
|
240
|
+
return response.json();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
app.get('/cached/*', async (request, reply) => {
|
|
244
|
+
const data = await cache.proxyGet(request.url);
|
|
245
|
+
return data;
|
|
246
|
+
});
|
|
247
|
+
```
|