@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,475 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: serialization
|
|
3
|
+
description: Response serialization in Fastify with TypeBox
|
|
4
|
+
metadata:
|
|
5
|
+
tags: serialization, response, json, fast-json-stringify, typebox
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Response Serialization
|
|
9
|
+
|
|
10
|
+
## Use TypeBox for Type-Safe Response Schemas
|
|
11
|
+
|
|
12
|
+
Define response schemas with TypeBox for automatic TypeScript types and fast serialization:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
17
|
+
|
|
18
|
+
const app = Fastify();
|
|
19
|
+
|
|
20
|
+
// Define response schema with TypeBox
|
|
21
|
+
const UserResponse = Type.Object({
|
|
22
|
+
id: Type.String(),
|
|
23
|
+
name: Type.String(),
|
|
24
|
+
email: Type.String(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const UsersResponse = Type.Array(UserResponse);
|
|
28
|
+
|
|
29
|
+
type UserResponseType = Static<typeof UserResponse>;
|
|
30
|
+
|
|
31
|
+
// With TypeBox schema - uses fast-json-stringify (faster) + TypeScript types
|
|
32
|
+
app.get<{ Reply: Static<typeof UsersResponse> }>('/users', {
|
|
33
|
+
schema: {
|
|
34
|
+
response: {
|
|
35
|
+
200: UsersResponse,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}, async () => {
|
|
39
|
+
return db.users.findAll();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Without schema - uses JSON.stringify (slower), no type safety
|
|
43
|
+
app.get('/users-slow', async () => {
|
|
44
|
+
return db.users.findAll();
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Fast JSON Stringify
|
|
49
|
+
|
|
50
|
+
Fastify uses `fast-json-stringify` when response schemas are defined. This provides:
|
|
51
|
+
|
|
52
|
+
1. **Performance**: 2-3x faster serialization than JSON.stringify
|
|
53
|
+
2. **Security**: Only defined properties are serialized (strips sensitive data)
|
|
54
|
+
3. **Type coercion**: Ensures output matches the schema
|
|
55
|
+
4. **TypeScript**: Full type inference with TypeBox
|
|
56
|
+
|
|
57
|
+
## Response Schema Benefits
|
|
58
|
+
|
|
59
|
+
1. **Performance**: 2-3x faster serialization
|
|
60
|
+
2. **Security**: Only defined properties are included
|
|
61
|
+
3. **Documentation**: OpenAPI/Swagger integration
|
|
62
|
+
4. **Type coercion**: Ensures correct output types
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
app.get('/user/:id', {
|
|
66
|
+
schema: {
|
|
67
|
+
response: {
|
|
68
|
+
200: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
id: { type: 'string' },
|
|
72
|
+
name: { type: 'string' },
|
|
73
|
+
// password is NOT in schema, so it's stripped
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}, async (request) => {
|
|
79
|
+
const user = await db.users.findById(request.params.id);
|
|
80
|
+
// Even if user has password field, it won't be serialized
|
|
81
|
+
return user;
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Multiple Status Code Schemas
|
|
86
|
+
|
|
87
|
+
Define schemas for different response codes:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
app.get('/users/:id', {
|
|
91
|
+
schema: {
|
|
92
|
+
response: {
|
|
93
|
+
200: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
id: { type: 'string' },
|
|
97
|
+
name: { type: 'string' },
|
|
98
|
+
email: { type: 'string' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
404: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
statusCode: { type: 'integer' },
|
|
105
|
+
error: { type: 'string' },
|
|
106
|
+
message: { type: 'string' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}, async (request, reply) => {
|
|
112
|
+
const user = await db.users.findById(request.params.id);
|
|
113
|
+
|
|
114
|
+
if (!user) {
|
|
115
|
+
reply.code(404);
|
|
116
|
+
return { statusCode: 404, error: 'Not Found', message: 'User not found' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return user;
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Default Response Schema
|
|
124
|
+
|
|
125
|
+
Use 'default' for common error responses:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
app.get('/resource', {
|
|
129
|
+
schema: {
|
|
130
|
+
response: {
|
|
131
|
+
200: { $ref: 'resource#' },
|
|
132
|
+
'4xx': {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
statusCode: { type: 'integer' },
|
|
136
|
+
error: { type: 'string' },
|
|
137
|
+
message: { type: 'string' },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
'5xx': {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
statusCode: { type: 'integer' },
|
|
144
|
+
error: { type: 'string' },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}, handler);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Custom Serializers
|
|
153
|
+
|
|
154
|
+
Create custom serialization functions:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Per-route serializer
|
|
158
|
+
app.get('/custom', {
|
|
159
|
+
schema: {
|
|
160
|
+
response: {
|
|
161
|
+
200: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
value: { type: 'string' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
serializerCompiler: ({ schema }) => {
|
|
170
|
+
return (data) => {
|
|
171
|
+
// Custom serialization logic
|
|
172
|
+
return JSON.stringify({
|
|
173
|
+
value: String(data.value).toUpperCase(),
|
|
174
|
+
serializedAt: new Date().toISOString(),
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
}, async () => {
|
|
179
|
+
return { value: 'hello' };
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Shared Serializers
|
|
184
|
+
|
|
185
|
+
Use the global serializer compiler:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import Fastify from 'fastify';
|
|
189
|
+
|
|
190
|
+
const app = Fastify({
|
|
191
|
+
serializerCompiler: ({ schema, method, url, httpStatus }) => {
|
|
192
|
+
// Custom compilation logic
|
|
193
|
+
const stringify = fastJson(schema);
|
|
194
|
+
return (data) => stringify(data);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Serialization with Type Coercion
|
|
200
|
+
|
|
201
|
+
fast-json-stringify coerces types:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
app.get('/data', {
|
|
205
|
+
schema: {
|
|
206
|
+
response: {
|
|
207
|
+
200: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
count: { type: 'integer' }, // '5' -> 5
|
|
211
|
+
active: { type: 'boolean' }, // 'true' -> true
|
|
212
|
+
tags: {
|
|
213
|
+
type: 'array',
|
|
214
|
+
items: { type: 'string' }, // [1, 2] -> ['1', '2']
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}, async () => {
|
|
221
|
+
return {
|
|
222
|
+
count: '5', // Coerced to integer
|
|
223
|
+
active: 'true', // Coerced to boolean
|
|
224
|
+
tags: [1, 2, 3], // Coerced to strings
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Nullable Fields
|
|
230
|
+
|
|
231
|
+
Handle nullable fields properly:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
app.get('/profile', {
|
|
235
|
+
schema: {
|
|
236
|
+
response: {
|
|
237
|
+
200: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
name: { type: 'string' },
|
|
241
|
+
bio: { type: ['string', 'null'] },
|
|
242
|
+
avatar: {
|
|
243
|
+
oneOf: [
|
|
244
|
+
{ type: 'string', format: 'uri' },
|
|
245
|
+
{ type: 'null' },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
}, async () => {
|
|
253
|
+
return {
|
|
254
|
+
name: 'John',
|
|
255
|
+
bio: null,
|
|
256
|
+
avatar: null,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Additional Properties
|
|
262
|
+
|
|
263
|
+
Control extra properties in response:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Strip additional properties (default)
|
|
267
|
+
app.get('/strict', {
|
|
268
|
+
schema: {
|
|
269
|
+
response: {
|
|
270
|
+
200: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
id: { type: 'string' },
|
|
274
|
+
name: { type: 'string' },
|
|
275
|
+
},
|
|
276
|
+
additionalProperties: false,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
}, async () => {
|
|
281
|
+
return { id: '1', name: 'John', secret: 'hidden' };
|
|
282
|
+
// Output: { "id": "1", "name": "John" }
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Allow additional properties
|
|
286
|
+
app.get('/flexible', {
|
|
287
|
+
schema: {
|
|
288
|
+
response: {
|
|
289
|
+
200: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
id: { type: 'string' },
|
|
293
|
+
},
|
|
294
|
+
additionalProperties: true,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
}, async () => {
|
|
299
|
+
return { id: '1', extra: 'included' };
|
|
300
|
+
// Output: { "id": "1", "extra": "included" }
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Nested Objects
|
|
305
|
+
|
|
306
|
+
Serialize nested structures:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
app.addSchema({
|
|
310
|
+
$id: 'address',
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
street: { type: 'string' },
|
|
314
|
+
city: { type: 'string' },
|
|
315
|
+
country: { type: 'string' },
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
app.get('/user', {
|
|
320
|
+
schema: {
|
|
321
|
+
response: {
|
|
322
|
+
200: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
name: { type: 'string' },
|
|
326
|
+
address: { $ref: 'address#' },
|
|
327
|
+
contacts: {
|
|
328
|
+
type: 'array',
|
|
329
|
+
items: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
type: { type: 'string' },
|
|
333
|
+
value: { type: 'string' },
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
}, async () => {
|
|
342
|
+
return {
|
|
343
|
+
name: 'John',
|
|
344
|
+
address: { street: '123 Main', city: 'Boston', country: 'USA' },
|
|
345
|
+
contacts: [
|
|
346
|
+
{ type: 'email', value: 'john@example.com' },
|
|
347
|
+
{ type: 'phone', value: '+1234567890' },
|
|
348
|
+
],
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Date Serialization
|
|
354
|
+
|
|
355
|
+
Handle dates consistently:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
app.get('/events', {
|
|
359
|
+
schema: {
|
|
360
|
+
response: {
|
|
361
|
+
200: {
|
|
362
|
+
type: 'array',
|
|
363
|
+
items: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
name: { type: 'string' },
|
|
367
|
+
date: { type: 'string', format: 'date-time' },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
}, async () => {
|
|
374
|
+
const events = await db.events.findAll();
|
|
375
|
+
|
|
376
|
+
// Convert Date objects to ISO strings
|
|
377
|
+
return events.map((e) => ({
|
|
378
|
+
...e,
|
|
379
|
+
date: e.date.toISOString(),
|
|
380
|
+
}));
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## BigInt Serialization
|
|
385
|
+
|
|
386
|
+
Handle BigInt values:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// BigInt is not JSON serializable by default
|
|
390
|
+
app.get('/large-number', {
|
|
391
|
+
schema: {
|
|
392
|
+
response: {
|
|
393
|
+
200: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
id: { type: 'string' }, // Serialize as string
|
|
397
|
+
count: { type: 'integer' },
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
}, async () => {
|
|
403
|
+
const bigValue = 9007199254740993n;
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
id: bigValue.toString(), // Convert to string
|
|
407
|
+
count: Number(bigValue), // Or number if safe
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Stream Responses
|
|
413
|
+
|
|
414
|
+
Stream responses bypass serialization:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import { createReadStream } from 'node:fs';
|
|
418
|
+
|
|
419
|
+
app.get('/file', async (request, reply) => {
|
|
420
|
+
const stream = createReadStream('./data.json');
|
|
421
|
+
reply.type('application/json');
|
|
422
|
+
return reply.send(stream);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Streaming JSON array
|
|
426
|
+
app.get('/stream', async (request, reply) => {
|
|
427
|
+
reply.type('application/json');
|
|
428
|
+
|
|
429
|
+
const cursor = db.users.findCursor();
|
|
430
|
+
|
|
431
|
+
reply.raw.write('[');
|
|
432
|
+
let first = true;
|
|
433
|
+
|
|
434
|
+
for await (const user of cursor) {
|
|
435
|
+
if (!first) reply.raw.write(',');
|
|
436
|
+
reply.raw.write(JSON.stringify(user));
|
|
437
|
+
first = false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
reply.raw.write(']');
|
|
441
|
+
reply.raw.end();
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Pre-Serialization Hook
|
|
446
|
+
|
|
447
|
+
Modify data before serialization:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
app.addHook('preSerialization', async (request, reply, payload) => {
|
|
451
|
+
// Add metadata to responses
|
|
452
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
453
|
+
return {
|
|
454
|
+
...payload,
|
|
455
|
+
_links: {
|
|
456
|
+
self: request.url,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return payload;
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Disable Serialization
|
|
465
|
+
|
|
466
|
+
Skip serialization for specific routes:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
app.get('/raw', async (request, reply) => {
|
|
470
|
+
const data = JSON.stringify({ raw: true });
|
|
471
|
+
reply.type('application/json');
|
|
472
|
+
reply.serializer((payload) => payload); // Pass through
|
|
473
|
+
return data;
|
|
474
|
+
});
|
|
475
|
+
```
|