@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,467 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: routes
|
|
3
|
+
description: Route organization and handlers in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: routes, handlers, http, rest, api
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Route Organization and Handlers
|
|
9
|
+
|
|
10
|
+
## Basic Route Definition
|
|
11
|
+
|
|
12
|
+
Define routes with the shorthand methods or the full route method:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
|
|
17
|
+
const app = Fastify();
|
|
18
|
+
|
|
19
|
+
// Shorthand methods
|
|
20
|
+
app.get('/users', async (request, reply) => {
|
|
21
|
+
return { users: [] };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.post('/users', async (request, reply) => {
|
|
25
|
+
return { created: true };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Full route method with all options
|
|
29
|
+
app.route({
|
|
30
|
+
method: 'GET',
|
|
31
|
+
url: '/users/:id',
|
|
32
|
+
schema: {
|
|
33
|
+
params: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
id: { type: 'string' },
|
|
37
|
+
},
|
|
38
|
+
required: ['id'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
handler: async (request, reply) => {
|
|
42
|
+
return { id: request.params.id };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Route Parameters
|
|
48
|
+
|
|
49
|
+
Access URL parameters through `request.params`:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Single parameter
|
|
53
|
+
app.get('/users/:id', async (request) => {
|
|
54
|
+
const { id } = request.params as { id: string };
|
|
55
|
+
return { userId: id };
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Multiple parameters
|
|
59
|
+
app.get('/users/:userId/posts/:postId', async (request) => {
|
|
60
|
+
const { userId, postId } = request.params as { userId: string; postId: string };
|
|
61
|
+
return { userId, postId };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Wildcard parameter (captures everything after)
|
|
65
|
+
app.get('/files/*', async (request) => {
|
|
66
|
+
const path = (request.params as { '*': string })['*'];
|
|
67
|
+
return { filePath: path };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Regex parameters (Fastify uses find-my-way)
|
|
71
|
+
app.get('/orders/:id(\\d+)', async (request) => {
|
|
72
|
+
// Only matches numeric IDs
|
|
73
|
+
const { id } = request.params as { id: string };
|
|
74
|
+
return { orderId: parseInt(id, 10) };
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Query String Parameters
|
|
79
|
+
|
|
80
|
+
Access query parameters through `request.query`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
app.get('/search', {
|
|
84
|
+
schema: {
|
|
85
|
+
querystring: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
q: { type: 'string' },
|
|
89
|
+
page: { type: 'integer', default: 1 },
|
|
90
|
+
limit: { type: 'integer', default: 10, maximum: 100 },
|
|
91
|
+
},
|
|
92
|
+
required: ['q'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
handler: async (request) => {
|
|
96
|
+
const { q, page, limit } = request.query as {
|
|
97
|
+
q: string;
|
|
98
|
+
page: number;
|
|
99
|
+
limit: number;
|
|
100
|
+
};
|
|
101
|
+
return { query: q, page, limit };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Request Body
|
|
107
|
+
|
|
108
|
+
Access the request body through `request.body`:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
app.post('/users', {
|
|
112
|
+
schema: {
|
|
113
|
+
body: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
name: { type: 'string', minLength: 1 },
|
|
117
|
+
email: { type: 'string', format: 'email' },
|
|
118
|
+
age: { type: 'integer', minimum: 0 },
|
|
119
|
+
},
|
|
120
|
+
required: ['name', 'email'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
handler: async (request, reply) => {
|
|
124
|
+
const user = request.body as { name: string; email: string; age?: number };
|
|
125
|
+
// Create user...
|
|
126
|
+
reply.code(201);
|
|
127
|
+
return { user };
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Headers
|
|
133
|
+
|
|
134
|
+
Access request headers through `request.headers`:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
app.get('/protected', {
|
|
138
|
+
schema: {
|
|
139
|
+
headers: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
authorization: { type: 'string' },
|
|
143
|
+
},
|
|
144
|
+
required: ['authorization'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
handler: async (request) => {
|
|
148
|
+
const token = request.headers.authorization;
|
|
149
|
+
return { authenticated: true };
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Reply Methods
|
|
155
|
+
|
|
156
|
+
Use reply methods to control the response:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
app.get('/examples', async (request, reply) => {
|
|
160
|
+
// Set status code
|
|
161
|
+
reply.code(201);
|
|
162
|
+
|
|
163
|
+
// Set headers
|
|
164
|
+
reply.header('X-Custom-Header', 'value');
|
|
165
|
+
reply.headers({ 'X-Another': 'value', 'X-Third': 'value' });
|
|
166
|
+
|
|
167
|
+
// Set content type
|
|
168
|
+
reply.type('application/json');
|
|
169
|
+
|
|
170
|
+
// Redirect
|
|
171
|
+
// reply.redirect('/other-url');
|
|
172
|
+
// reply.redirect(301, '/permanent-redirect');
|
|
173
|
+
|
|
174
|
+
// Return response (automatic serialization)
|
|
175
|
+
return { status: 'ok' };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Explicit send (useful in non-async handlers)
|
|
179
|
+
app.get('/explicit', (request, reply) => {
|
|
180
|
+
reply.send({ status: 'ok' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Stream response
|
|
184
|
+
app.get('/stream', async (request, reply) => {
|
|
185
|
+
const stream = fs.createReadStream('./large-file.txt');
|
|
186
|
+
reply.type('text/plain');
|
|
187
|
+
return reply.send(stream);
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Route Organization by Feature
|
|
192
|
+
|
|
193
|
+
Organize routes by feature/domain in separate files:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
src/
|
|
197
|
+
routes/
|
|
198
|
+
users/
|
|
199
|
+
index.ts # Route definitions
|
|
200
|
+
handlers.ts # Handler functions
|
|
201
|
+
schemas.ts # JSON schemas
|
|
202
|
+
posts/
|
|
203
|
+
index.ts
|
|
204
|
+
handlers.ts
|
|
205
|
+
schemas.ts
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// routes/users/schemas.ts
|
|
210
|
+
export const userSchema = {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
id: { type: 'string', format: 'uuid' },
|
|
214
|
+
name: { type: 'string' },
|
|
215
|
+
email: { type: 'string', format: 'email' },
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const createUserSchema = {
|
|
220
|
+
body: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
name: { type: 'string', minLength: 1 },
|
|
224
|
+
email: { type: 'string', format: 'email' },
|
|
225
|
+
},
|
|
226
|
+
required: ['name', 'email'],
|
|
227
|
+
},
|
|
228
|
+
response: {
|
|
229
|
+
201: userSchema,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// routes/users/handlers.ts
|
|
234
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
235
|
+
|
|
236
|
+
export async function createUser(
|
|
237
|
+
request: FastifyRequest<{ Body: { name: string; email: string } }>,
|
|
238
|
+
reply: FastifyReply,
|
|
239
|
+
) {
|
|
240
|
+
const { name, email } = request.body;
|
|
241
|
+
const user = await request.server.db.users.create({ name, email });
|
|
242
|
+
reply.code(201);
|
|
243
|
+
return user;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function getUsers(request: FastifyRequest) {
|
|
247
|
+
return request.server.db.users.findAll();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// routes/users/index.ts
|
|
251
|
+
import type { FastifyInstance } from 'fastify';
|
|
252
|
+
import { createUser, getUsers } from './handlers.js';
|
|
253
|
+
import { createUserSchema } from './schemas.js';
|
|
254
|
+
|
|
255
|
+
export default async function userRoutes(fastify: FastifyInstance) {
|
|
256
|
+
fastify.get('/', getUsers);
|
|
257
|
+
fastify.post('/', { schema: createUserSchema }, createUser);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Route Constraints
|
|
262
|
+
|
|
263
|
+
Add constraints to routes for versioning or host-based routing:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Version constraint
|
|
267
|
+
app.get('/users', {
|
|
268
|
+
constraints: { version: '1.0.0' },
|
|
269
|
+
handler: async () => ({ version: '1.0.0', users: [] }),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
app.get('/users', {
|
|
273
|
+
constraints: { version: '2.0.0' },
|
|
274
|
+
handler: async () => ({ version: '2.0.0', data: { users: [] } }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Client sends: Accept-Version: 1.0.0
|
|
278
|
+
|
|
279
|
+
// Host constraint
|
|
280
|
+
app.get('/', {
|
|
281
|
+
constraints: { host: 'api.example.com' },
|
|
282
|
+
handler: async () => ({ api: true }),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
app.get('/', {
|
|
286
|
+
constraints: { host: 'www.example.com' },
|
|
287
|
+
handler: async () => ({ web: true }),
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Route Prefixing
|
|
292
|
+
|
|
293
|
+
Use prefixes to namespace routes:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Using register
|
|
297
|
+
app.register(async function (fastify) {
|
|
298
|
+
fastify.get('/list', async () => ({ users: [] }));
|
|
299
|
+
fastify.get('/:id', async (request) => ({ id: request.params.id }));
|
|
300
|
+
}, { prefix: '/users' });
|
|
301
|
+
|
|
302
|
+
// Results in:
|
|
303
|
+
// GET /users/list
|
|
304
|
+
// GET /users/:id
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Multiple Methods
|
|
308
|
+
|
|
309
|
+
Handle multiple HTTP methods with one handler:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
app.route({
|
|
313
|
+
method: ['GET', 'HEAD'],
|
|
314
|
+
url: '/resource',
|
|
315
|
+
handler: async (request) => {
|
|
316
|
+
return { data: 'resource' };
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## 404 Handler
|
|
322
|
+
|
|
323
|
+
Customize the not found handler:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
app.setNotFoundHandler({
|
|
327
|
+
preValidation: async (request, reply) => {
|
|
328
|
+
// Optional pre-validation hook
|
|
329
|
+
},
|
|
330
|
+
preHandler: async (request, reply) => {
|
|
331
|
+
// Optional pre-handler hook
|
|
332
|
+
},
|
|
333
|
+
}, async (request, reply) => {
|
|
334
|
+
reply.code(404);
|
|
335
|
+
return {
|
|
336
|
+
error: 'Not Found',
|
|
337
|
+
message: `Route ${request.method} ${request.url} not found`,
|
|
338
|
+
statusCode: 404,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Method Not Allowed
|
|
344
|
+
|
|
345
|
+
Handle method not allowed responses:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Fastify doesn't have built-in 405 handling
|
|
349
|
+
// Implement with a custom not found handler that checks allowed methods
|
|
350
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
351
|
+
// Check if the URL exists with a different method
|
|
352
|
+
const route = app.hasRoute({
|
|
353
|
+
url: request.url,
|
|
354
|
+
method: 'GET', // Check other methods
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (route) {
|
|
358
|
+
reply.code(405);
|
|
359
|
+
return { error: 'Method Not Allowed' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
reply.code(404);
|
|
363
|
+
return { error: 'Not Found' };
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Route-Level Configuration
|
|
368
|
+
|
|
369
|
+
Apply configuration to specific routes:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
app.get('/slow-operation', {
|
|
373
|
+
config: {
|
|
374
|
+
rateLimit: { max: 10, timeWindow: '1 minute' },
|
|
375
|
+
},
|
|
376
|
+
handler: async (request) => {
|
|
377
|
+
return { result: await slowOperation() };
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Access config in hooks
|
|
382
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
383
|
+
const config = request.routeOptions.config;
|
|
384
|
+
if (config.rateLimit) {
|
|
385
|
+
// Apply rate limiting
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Async Route Registration
|
|
391
|
+
|
|
392
|
+
Register routes from async sources:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
app.register(async function (fastify) {
|
|
396
|
+
const routeConfigs = await loadRoutesFromDatabase();
|
|
397
|
+
|
|
398
|
+
for (const config of routeConfigs) {
|
|
399
|
+
fastify.route({
|
|
400
|
+
method: config.method,
|
|
401
|
+
url: config.path,
|
|
402
|
+
handler: createDynamicHandler(config),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Auto-loading Routes with @fastify/autoload
|
|
409
|
+
|
|
410
|
+
Use `@fastify/autoload` to automatically load routes from a directory structure:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import Fastify from 'fastify';
|
|
414
|
+
import autoload from '@fastify/autoload';
|
|
415
|
+
import { join } from 'node:path';
|
|
416
|
+
|
|
417
|
+
const app = Fastify({ logger: true });
|
|
418
|
+
|
|
419
|
+
// Auto-load plugins
|
|
420
|
+
app.register(autoload, {
|
|
421
|
+
dir: join(import.meta.dirname, 'plugins'),
|
|
422
|
+
options: { prefix: '' },
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Auto-load routes
|
|
426
|
+
app.register(autoload, {
|
|
427
|
+
dir: join(import.meta.dirname, 'routes'),
|
|
428
|
+
options: { prefix: '/api' },
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await app.listen({ port: 3000 });
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Directory structure:
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
src/
|
|
438
|
+
plugins/
|
|
439
|
+
database.ts # Loaded automatically
|
|
440
|
+
auth.ts # Loaded automatically
|
|
441
|
+
routes/
|
|
442
|
+
users/
|
|
443
|
+
index.ts # GET/POST /api/users
|
|
444
|
+
_id/
|
|
445
|
+
index.ts # GET/PUT/DELETE /api/users/:id
|
|
446
|
+
posts/
|
|
447
|
+
index.ts # GET/POST /api/posts
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Route file example:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// routes/users/index.ts
|
|
454
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
455
|
+
|
|
456
|
+
const users: FastifyPluginAsync = async (fastify) => {
|
|
457
|
+
fastify.get('/', async () => {
|
|
458
|
+
return fastify.repositories.users.findAll();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
fastify.post('/', async (request) => {
|
|
462
|
+
return fastify.repositories.users.create(request.body);
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
export default users;
|
|
467
|
+
```
|