@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,585 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: schemas
|
|
3
|
+
description: JSON Schema validation in Fastify with TypeBox
|
|
4
|
+
metadata:
|
|
5
|
+
tags: validation, json-schema, schemas, ajv, typebox
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# JSON Schema Validation
|
|
9
|
+
|
|
10
|
+
## Use TypeBox for Type-Safe Schemas
|
|
11
|
+
|
|
12
|
+
**Prefer TypeBox for defining schemas.** It provides TypeScript types automatically and compiles to JSON Schema:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
17
|
+
|
|
18
|
+
const app = Fastify();
|
|
19
|
+
|
|
20
|
+
// Define schema with TypeBox - get TypeScript types for free
|
|
21
|
+
const CreateUserBody = Type.Object({
|
|
22
|
+
name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
23
|
+
email: Type.String({ format: 'email' }),
|
|
24
|
+
age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const UserResponse = Type.Object({
|
|
28
|
+
id: Type.String({ format: 'uuid' }),
|
|
29
|
+
name: Type.String(),
|
|
30
|
+
email: Type.String(),
|
|
31
|
+
createdAt: Type.String({ format: 'date-time' }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// TypeScript types are derived automatically
|
|
35
|
+
type CreateUserBodyType = Static<typeof CreateUserBody>;
|
|
36
|
+
type UserResponseType = Static<typeof UserResponse>;
|
|
37
|
+
|
|
38
|
+
app.post<{
|
|
39
|
+
Body: CreateUserBodyType;
|
|
40
|
+
Reply: UserResponseType;
|
|
41
|
+
}>('/users', {
|
|
42
|
+
schema: {
|
|
43
|
+
body: CreateUserBody,
|
|
44
|
+
response: {
|
|
45
|
+
201: UserResponse,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}, async (request, reply) => {
|
|
49
|
+
// request.body is fully typed as CreateUserBodyType
|
|
50
|
+
const user = await createUser(request.body);
|
|
51
|
+
reply.code(201);
|
|
52
|
+
return user;
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## TypeBox Common Patterns
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
60
|
+
|
|
61
|
+
// Enums
|
|
62
|
+
const Status = Type.Union([
|
|
63
|
+
Type.Literal('active'),
|
|
64
|
+
Type.Literal('inactive'),
|
|
65
|
+
Type.Literal('pending'),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// Arrays
|
|
69
|
+
const Tags = Type.Array(Type.String(), { minItems: 1, maxItems: 10 });
|
|
70
|
+
|
|
71
|
+
// Nested objects
|
|
72
|
+
const Address = Type.Object({
|
|
73
|
+
street: Type.String(),
|
|
74
|
+
city: Type.String(),
|
|
75
|
+
country: Type.String(),
|
|
76
|
+
zip: Type.Optional(Type.String()),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// References (reusable schemas)
|
|
80
|
+
const User = Type.Object({
|
|
81
|
+
id: Type.String({ format: 'uuid' }),
|
|
82
|
+
name: Type.String(),
|
|
83
|
+
address: Address,
|
|
84
|
+
tags: Tags,
|
|
85
|
+
status: Status,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Nullable
|
|
89
|
+
const NullableString = Type.Union([Type.String(), Type.Null()]);
|
|
90
|
+
|
|
91
|
+
// Record/Map
|
|
92
|
+
const Metadata = Type.Record(Type.String(), Type.Unknown());
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Register TypeBox Schemas Globally
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
99
|
+
|
|
100
|
+
// Define shared schemas
|
|
101
|
+
const ErrorResponse = Type.Object({
|
|
102
|
+
error: Type.String(),
|
|
103
|
+
message: Type.String(),
|
|
104
|
+
statusCode: Type.Integer(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const PaginationQuery = Type.Object({
|
|
108
|
+
page: Type.Integer({ minimum: 1, default: 1 }),
|
|
109
|
+
limit: Type.Integer({ minimum: 1, maximum: 100, default: 20 }),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Register globally
|
|
113
|
+
app.addSchema(Type.Object({ $id: 'ErrorResponse', ...ErrorResponse }));
|
|
114
|
+
app.addSchema(Type.Object({ $id: 'PaginationQuery', ...PaginationQuery }));
|
|
115
|
+
|
|
116
|
+
// Reference in routes
|
|
117
|
+
app.get('/items', {
|
|
118
|
+
schema: {
|
|
119
|
+
querystring: { $ref: 'PaginationQuery#' },
|
|
120
|
+
response: {
|
|
121
|
+
400: { $ref: 'ErrorResponse#' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}, handler);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Plain JSON Schema (Alternative)
|
|
128
|
+
|
|
129
|
+
You can also use plain JSON Schema directly:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import Fastify from 'fastify';
|
|
133
|
+
|
|
134
|
+
const app = Fastify();
|
|
135
|
+
|
|
136
|
+
const createUserSchema = {
|
|
137
|
+
body: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
141
|
+
email: { type: 'string', format: 'email' },
|
|
142
|
+
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
143
|
+
},
|
|
144
|
+
required: ['name', 'email'],
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
},
|
|
147
|
+
response: {
|
|
148
|
+
201: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
id: { type: 'string', format: 'uuid' },
|
|
152
|
+
name: { type: 'string' },
|
|
153
|
+
email: { type: 'string' },
|
|
154
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
|
|
161
|
+
const user = await createUser(request.body);
|
|
162
|
+
reply.code(201);
|
|
163
|
+
return user;
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Request Validation Parts
|
|
168
|
+
|
|
169
|
+
Validate different parts of the request:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const fullRequestSchema = {
|
|
173
|
+
// URL parameters
|
|
174
|
+
params: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
id: { type: 'string', format: 'uuid' },
|
|
178
|
+
},
|
|
179
|
+
required: ['id'],
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Query string
|
|
183
|
+
querystring: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
include: { type: 'string', enum: ['posts', 'comments', 'all'] },
|
|
187
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Request headers
|
|
192
|
+
headers: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
'x-api-key': { type: 'string', minLength: 32 },
|
|
196
|
+
},
|
|
197
|
+
required: ['x-api-key'],
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// Request body
|
|
201
|
+
body: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
data: { type: 'object' },
|
|
205
|
+
},
|
|
206
|
+
required: ['data'],
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
app.put('/resources/:id', { schema: fullRequestSchema }, handler);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Shared Schemas with $id
|
|
214
|
+
|
|
215
|
+
Define reusable schemas with `$id` and reference them with `$ref`:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// Add shared schemas to Fastify
|
|
219
|
+
app.addSchema({
|
|
220
|
+
$id: 'user',
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
id: { type: 'string', format: 'uuid' },
|
|
224
|
+
name: { type: 'string' },
|
|
225
|
+
email: { type: 'string', format: 'email' },
|
|
226
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
227
|
+
},
|
|
228
|
+
required: ['id', 'name', 'email'],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
app.addSchema({
|
|
232
|
+
$id: 'userCreate',
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
name: { type: 'string', minLength: 1 },
|
|
236
|
+
email: { type: 'string', format: 'email' },
|
|
237
|
+
},
|
|
238
|
+
required: ['name', 'email'],
|
|
239
|
+
additionalProperties: false,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
app.addSchema({
|
|
243
|
+
$id: 'error',
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
statusCode: { type: 'integer' },
|
|
247
|
+
error: { type: 'string' },
|
|
248
|
+
message: { type: 'string' },
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Reference shared schemas
|
|
253
|
+
app.post('/users', {
|
|
254
|
+
schema: {
|
|
255
|
+
body: { $ref: 'userCreate#' },
|
|
256
|
+
response: {
|
|
257
|
+
201: { $ref: 'user#' },
|
|
258
|
+
400: { $ref: 'error#' },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
}, handler);
|
|
262
|
+
|
|
263
|
+
app.get('/users/:id', {
|
|
264
|
+
schema: {
|
|
265
|
+
params: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: { id: { type: 'string', format: 'uuid' } },
|
|
268
|
+
required: ['id'],
|
|
269
|
+
},
|
|
270
|
+
response: {
|
|
271
|
+
200: { $ref: 'user#' },
|
|
272
|
+
404: { $ref: 'error#' },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
}, handler);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Array Schemas
|
|
279
|
+
|
|
280
|
+
Define schemas for array responses:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
app.addSchema({
|
|
284
|
+
$id: 'userList',
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
users: {
|
|
288
|
+
type: 'array',
|
|
289
|
+
items: { $ref: 'user#' },
|
|
290
|
+
},
|
|
291
|
+
total: { type: 'integer' },
|
|
292
|
+
page: { type: 'integer' },
|
|
293
|
+
pageSize: { type: 'integer' },
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
app.get('/users', {
|
|
298
|
+
schema: {
|
|
299
|
+
querystring: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: {
|
|
302
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
303
|
+
pageSize: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
response: {
|
|
307
|
+
200: { $ref: 'userList#' },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
}, handler);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Custom Formats
|
|
314
|
+
|
|
315
|
+
Add custom validation formats:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import Fastify from 'fastify';
|
|
319
|
+
|
|
320
|
+
const app = Fastify({
|
|
321
|
+
ajv: {
|
|
322
|
+
customOptions: {
|
|
323
|
+
formats: {
|
|
324
|
+
'iso-country': /^[A-Z]{2}$/,
|
|
325
|
+
'phone': /^\+?[1-9]\d{1,14}$/,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Or add formats dynamically
|
|
332
|
+
app.addSchema({
|
|
333
|
+
$id: 'address',
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
street: { type: 'string' },
|
|
337
|
+
country: { type: 'string', format: 'iso-country' },
|
|
338
|
+
phone: { type: 'string', format: 'phone' },
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Custom Keywords
|
|
344
|
+
|
|
345
|
+
Add custom validation keywords:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import Fastify from 'fastify';
|
|
349
|
+
import Ajv from 'ajv';
|
|
350
|
+
|
|
351
|
+
const app = Fastify({
|
|
352
|
+
ajv: {
|
|
353
|
+
customOptions: {
|
|
354
|
+
keywords: [
|
|
355
|
+
{
|
|
356
|
+
keyword: 'isEven',
|
|
357
|
+
type: 'number',
|
|
358
|
+
validate: (schema: boolean, data: number) => {
|
|
359
|
+
if (schema) {
|
|
360
|
+
return data % 2 === 0;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
},
|
|
364
|
+
errors: false,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Use custom keyword
|
|
372
|
+
app.post('/numbers', {
|
|
373
|
+
schema: {
|
|
374
|
+
body: {
|
|
375
|
+
type: 'object',
|
|
376
|
+
properties: {
|
|
377
|
+
value: { type: 'integer', isEven: true },
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
}, handler);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Coercion
|
|
385
|
+
|
|
386
|
+
Fastify coerces types by default for query strings and params:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// Query string "?page=5&active=true" becomes:
|
|
390
|
+
// { page: 5, active: true } (number and boolean, not strings)
|
|
391
|
+
|
|
392
|
+
app.get('/items', {
|
|
393
|
+
schema: {
|
|
394
|
+
querystring: {
|
|
395
|
+
type: 'object',
|
|
396
|
+
properties: {
|
|
397
|
+
page: { type: 'integer' }, // "5" -> 5
|
|
398
|
+
active: { type: 'boolean' }, // "true" -> true
|
|
399
|
+
tags: {
|
|
400
|
+
type: 'array',
|
|
401
|
+
items: { type: 'string' }, // "a,b,c" -> ["a", "b", "c"]
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
}, handler);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Validation Error Handling
|
|
410
|
+
|
|
411
|
+
Customize validation error responses:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
app.setErrorHandler((error, request, reply) => {
|
|
415
|
+
if (error.validation) {
|
|
416
|
+
reply.code(400).send({
|
|
417
|
+
error: 'Validation Error',
|
|
418
|
+
message: 'Request validation failed',
|
|
419
|
+
details: error.validation.map((err) => ({
|
|
420
|
+
field: err.instancePath || err.params?.missingProperty,
|
|
421
|
+
message: err.message,
|
|
422
|
+
keyword: err.keyword,
|
|
423
|
+
})),
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Handle other errors
|
|
429
|
+
reply.code(error.statusCode || 500).send({
|
|
430
|
+
error: error.name,
|
|
431
|
+
message: error.message,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## Schema Compiler Options
|
|
437
|
+
|
|
438
|
+
Configure the Ajv schema compiler:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import Fastify from 'fastify';
|
|
442
|
+
|
|
443
|
+
const app = Fastify({
|
|
444
|
+
ajv: {
|
|
445
|
+
customOptions: {
|
|
446
|
+
removeAdditional: 'all', // Remove extra properties
|
|
447
|
+
useDefaults: true, // Apply default values
|
|
448
|
+
coerceTypes: true, // Coerce types
|
|
449
|
+
allErrors: true, // Report all errors, not just first
|
|
450
|
+
},
|
|
451
|
+
plugins: [
|
|
452
|
+
require('ajv-formats'), // Add format validators
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Nullable Fields
|
|
459
|
+
|
|
460
|
+
Handle nullable fields properly:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
app.addSchema({
|
|
464
|
+
$id: 'profile',
|
|
465
|
+
type: 'object',
|
|
466
|
+
properties: {
|
|
467
|
+
name: { type: 'string' },
|
|
468
|
+
bio: { type: ['string', 'null'] }, // Can be string or null
|
|
469
|
+
avatar: {
|
|
470
|
+
oneOf: [
|
|
471
|
+
{ type: 'string', format: 'uri' },
|
|
472
|
+
{ type: 'null' },
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Conditional Validation
|
|
480
|
+
|
|
481
|
+
Use if/then/else for conditional validation:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
app.addSchema({
|
|
485
|
+
$id: 'payment',
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {
|
|
488
|
+
method: { type: 'string', enum: ['card', 'bank'] },
|
|
489
|
+
cardNumber: { type: 'string' },
|
|
490
|
+
bankAccount: { type: 'string' },
|
|
491
|
+
},
|
|
492
|
+
required: ['method'],
|
|
493
|
+
if: {
|
|
494
|
+
properties: { method: { const: 'card' } },
|
|
495
|
+
},
|
|
496
|
+
then: {
|
|
497
|
+
required: ['cardNumber'],
|
|
498
|
+
},
|
|
499
|
+
else: {
|
|
500
|
+
required: ['bankAccount'],
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Schema Organization
|
|
506
|
+
|
|
507
|
+
Organize schemas in a dedicated file:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// schemas/index.ts
|
|
511
|
+
export const schemas = [
|
|
512
|
+
{
|
|
513
|
+
$id: 'user',
|
|
514
|
+
type: 'object',
|
|
515
|
+
properties: {
|
|
516
|
+
id: { type: 'string', format: 'uuid' },
|
|
517
|
+
name: { type: 'string' },
|
|
518
|
+
email: { type: 'string', format: 'email' },
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
$id: 'error',
|
|
523
|
+
type: 'object',
|
|
524
|
+
properties: {
|
|
525
|
+
statusCode: { type: 'integer' },
|
|
526
|
+
error: { type: 'string' },
|
|
527
|
+
message: { type: 'string' },
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
];
|
|
531
|
+
|
|
532
|
+
// app.ts
|
|
533
|
+
import { schemas } from './schemas/index.js';
|
|
534
|
+
|
|
535
|
+
for (const schema of schemas) {
|
|
536
|
+
app.addSchema(schema);
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## OpenAPI/Swagger Integration
|
|
541
|
+
|
|
542
|
+
Schemas work directly with @fastify/swagger:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
import fastifySwagger from '@fastify/swagger';
|
|
546
|
+
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
547
|
+
|
|
548
|
+
app.register(fastifySwagger, {
|
|
549
|
+
openapi: {
|
|
550
|
+
info: {
|
|
551
|
+
title: 'My API',
|
|
552
|
+
version: '1.0.0',
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
app.register(fastifySwaggerUi, {
|
|
558
|
+
routePrefix: '/docs',
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Schemas are automatically converted to OpenAPI definitions
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
## Performance Considerations
|
|
565
|
+
|
|
566
|
+
Response schemas enable fast-json-stringify for serialization:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
// With response schema - uses fast-json-stringify (faster)
|
|
570
|
+
app.get('/users', {
|
|
571
|
+
schema: {
|
|
572
|
+
response: {
|
|
573
|
+
200: {
|
|
574
|
+
type: 'array',
|
|
575
|
+
items: { $ref: 'user#' },
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
}, handler);
|
|
580
|
+
|
|
581
|
+
// Without response schema - uses JSON.stringify (slower)
|
|
582
|
+
app.get('/users-slow', handler);
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Always define response schemas for production APIs to benefit from optimized serialization.
|