@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,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configuration
|
|
3
|
+
description: Application configuration in Fastify using env-schema
|
|
4
|
+
metadata:
|
|
5
|
+
tags: configuration, environment, env, settings, env-schema
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Application Configuration
|
|
9
|
+
|
|
10
|
+
## Use env-schema for Configuration
|
|
11
|
+
|
|
12
|
+
**Always use `env-schema` for configuration validation.** It provides JSON Schema validation for environment variables with sensible defaults.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import envSchema from 'env-schema';
|
|
17
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
18
|
+
|
|
19
|
+
const schema = Type.Object({
|
|
20
|
+
PORT: Type.Number({ default: 3000 }),
|
|
21
|
+
HOST: Type.String({ default: '0.0.0.0' }),
|
|
22
|
+
DATABASE_URL: Type.String(),
|
|
23
|
+
JWT_SECRET: Type.String({ minLength: 32 }),
|
|
24
|
+
LOG_LEVEL: Type.Union([
|
|
25
|
+
Type.Literal('trace'),
|
|
26
|
+
Type.Literal('debug'),
|
|
27
|
+
Type.Literal('info'),
|
|
28
|
+
Type.Literal('warn'),
|
|
29
|
+
Type.Literal('error'),
|
|
30
|
+
Type.Literal('fatal'),
|
|
31
|
+
], { default: 'info' }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type Config = Static<typeof schema>;
|
|
35
|
+
|
|
36
|
+
const config = envSchema<Config>({
|
|
37
|
+
schema,
|
|
38
|
+
dotenv: true, // Load from .env file
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const app = Fastify({
|
|
42
|
+
logger: { level: config.LOG_LEVEL },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.decorate('config', config);
|
|
46
|
+
|
|
47
|
+
declare module 'fastify' {
|
|
48
|
+
interface FastifyInstance {
|
|
49
|
+
config: Config;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await app.listen({ port: config.PORT, host: config.HOST });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration as Plugin
|
|
57
|
+
|
|
58
|
+
Encapsulate configuration in a plugin for reuse:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import fp from 'fastify-plugin';
|
|
62
|
+
import envSchema from 'env-schema';
|
|
63
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
64
|
+
|
|
65
|
+
const schema = Type.Object({
|
|
66
|
+
PORT: Type.Number({ default: 3000 }),
|
|
67
|
+
HOST: Type.String({ default: '0.0.0.0' }),
|
|
68
|
+
DATABASE_URL: Type.String(),
|
|
69
|
+
JWT_SECRET: Type.String({ minLength: 32 }),
|
|
70
|
+
LOG_LEVEL: Type.String({ default: 'info' }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
type Config = Static<typeof schema>;
|
|
74
|
+
|
|
75
|
+
declare module 'fastify' {
|
|
76
|
+
interface FastifyInstance {
|
|
77
|
+
config: Config;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default fp(async function configPlugin(fastify) {
|
|
82
|
+
const config = envSchema<Config>({
|
|
83
|
+
schema,
|
|
84
|
+
dotenv: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
fastify.decorate('config', config);
|
|
88
|
+
}, {
|
|
89
|
+
name: 'config',
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Secrets Management
|
|
94
|
+
|
|
95
|
+
Handle secrets securely:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Never log secrets
|
|
99
|
+
const app = Fastify({
|
|
100
|
+
logger: {
|
|
101
|
+
level: config.LOG_LEVEL,
|
|
102
|
+
redact: ['req.headers.authorization', '*.password', '*.secret', '*.apiKey'],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// For production, use secret managers (AWS Secrets Manager, Vault, etc.)
|
|
107
|
+
// Pass secrets through environment variables - never commit them
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Feature Flags
|
|
111
|
+
|
|
112
|
+
Implement feature flags via environment variables:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
116
|
+
|
|
117
|
+
const schema = Type.Object({
|
|
118
|
+
// ... other config
|
|
119
|
+
FEATURE_NEW_DASHBOARD: Type.Boolean({ default: false }),
|
|
120
|
+
FEATURE_BETA_API: Type.Boolean({ default: false }),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
type Config = Static<typeof schema>;
|
|
124
|
+
|
|
125
|
+
const config = envSchema<Config>({ schema, dotenv: true });
|
|
126
|
+
|
|
127
|
+
// Use in routes
|
|
128
|
+
app.get('/dashboard', async (request) => {
|
|
129
|
+
if (app.config.FEATURE_NEW_DASHBOARD) {
|
|
130
|
+
return { version: 'v2', data: await getNewDashboardData() };
|
|
131
|
+
}
|
|
132
|
+
return { version: 'v1', data: await getOldDashboardData() };
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Anti-Patterns to Avoid
|
|
137
|
+
|
|
138
|
+
### NEVER use configuration files
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ❌ NEVER DO THIS - configuration files are an antipattern
|
|
142
|
+
import config from './config/production.json';
|
|
143
|
+
|
|
144
|
+
// ❌ NEVER DO THIS - per-environment config files
|
|
145
|
+
const env = process.env.NODE_ENV || 'development';
|
|
146
|
+
const config = await import(`./config/${env}.js`);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Configuration files lead to:
|
|
150
|
+
- Security risks (secrets in files)
|
|
151
|
+
- Deployment complexity
|
|
152
|
+
- Environment drift
|
|
153
|
+
- Difficult secret rotation
|
|
154
|
+
|
|
155
|
+
### NEVER use per-environment configuration
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// ❌ NEVER DO THIS
|
|
159
|
+
const configs = {
|
|
160
|
+
development: { logLevel: 'debug' },
|
|
161
|
+
production: { logLevel: 'info' },
|
|
162
|
+
test: { logLevel: 'silent' },
|
|
163
|
+
};
|
|
164
|
+
const config = configs[process.env.NODE_ENV];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Instead, use a single configuration source (environment variables) with sensible defaults. The environment controls the values, not conditional code.
|
|
168
|
+
|
|
169
|
+
### Use specific environment variables, not NODE_ENV
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// ❌ AVOID checking NODE_ENV
|
|
173
|
+
if (process.env.NODE_ENV === 'production') {
|
|
174
|
+
// do something
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ✅ BETTER - use explicit feature flags or configuration
|
|
178
|
+
if (app.config.ENABLE_DETAILED_LOGGING) {
|
|
179
|
+
// do something
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Dynamic Configuration
|
|
184
|
+
|
|
185
|
+
For configuration that needs to change without restart, fetch from an external service:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface DynamicConfig {
|
|
189
|
+
rateLimit: number;
|
|
190
|
+
maintenanceMode: boolean;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let dynamicConfig: DynamicConfig = {
|
|
194
|
+
rateLimit: 100,
|
|
195
|
+
maintenanceMode: false,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
async function refreshConfig() {
|
|
199
|
+
try {
|
|
200
|
+
const newConfig = await fetchConfigFromService();
|
|
201
|
+
dynamicConfig = newConfig;
|
|
202
|
+
app.log.info('Configuration refreshed');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
app.log.error({ err: error }, 'Failed to refresh configuration');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Refresh periodically
|
|
209
|
+
setInterval(refreshConfig, 60000);
|
|
210
|
+
|
|
211
|
+
// Use in hooks
|
|
212
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
213
|
+
if (dynamicConfig.maintenanceMode && !request.url.startsWith('/health')) {
|
|
214
|
+
reply.code(503).send({ error: 'Service under maintenance' });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
```
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: content-type
|
|
3
|
+
description: Content type parsing in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: content-type, parsing, body, multipart, json
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Content Type Parsing
|
|
9
|
+
|
|
10
|
+
## Default Content Type Parsers
|
|
11
|
+
|
|
12
|
+
Fastify includes parsers for common content types:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
|
|
17
|
+
const app = Fastify();
|
|
18
|
+
|
|
19
|
+
// Built-in parsers:
|
|
20
|
+
// - application/json
|
|
21
|
+
// - text/plain
|
|
22
|
+
|
|
23
|
+
app.post('/json', async (request) => {
|
|
24
|
+
// request.body is parsed JSON object
|
|
25
|
+
return { received: request.body };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.post('/text', async (request) => {
|
|
29
|
+
// request.body is string for text/plain
|
|
30
|
+
return { text: request.body };
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Custom Content Type Parsers
|
|
35
|
+
|
|
36
|
+
Add parsers for additional content types:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Parse application/x-www-form-urlencoded
|
|
40
|
+
app.addContentTypeParser(
|
|
41
|
+
'application/x-www-form-urlencoded',
|
|
42
|
+
{ parseAs: 'string' },
|
|
43
|
+
(request, body, done) => {
|
|
44
|
+
const parsed = new URLSearchParams(body);
|
|
45
|
+
done(null, Object.fromEntries(parsed));
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Async parser
|
|
50
|
+
app.addContentTypeParser(
|
|
51
|
+
'application/x-www-form-urlencoded',
|
|
52
|
+
{ parseAs: 'string' },
|
|
53
|
+
async (request, body) => {
|
|
54
|
+
const parsed = new URLSearchParams(body);
|
|
55
|
+
return Object.fromEntries(parsed);
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## XML Parsing
|
|
61
|
+
|
|
62
|
+
Parse XML content:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
66
|
+
|
|
67
|
+
const xmlParser = new XMLParser({
|
|
68
|
+
ignoreAttributes: false,
|
|
69
|
+
attributeNamePrefix: '@_',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.addContentTypeParser(
|
|
73
|
+
'application/xml',
|
|
74
|
+
{ parseAs: 'string' },
|
|
75
|
+
async (request, body) => {
|
|
76
|
+
return xmlParser.parse(body);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
app.addContentTypeParser(
|
|
81
|
+
'text/xml',
|
|
82
|
+
{ parseAs: 'string' },
|
|
83
|
+
async (request, body) => {
|
|
84
|
+
return xmlParser.parse(body);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
app.post('/xml', async (request) => {
|
|
89
|
+
// request.body is parsed XML as JavaScript object
|
|
90
|
+
return { data: request.body };
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Multipart Form Data
|
|
95
|
+
|
|
96
|
+
Use @fastify/multipart for file uploads. **Configure these critical options:**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import fastifyMultipart from '@fastify/multipart';
|
|
100
|
+
|
|
101
|
+
app.register(fastifyMultipart, {
|
|
102
|
+
// CRITICAL: Always set explicit limits
|
|
103
|
+
limits: {
|
|
104
|
+
fieldNameSize: 100, // Max field name size in bytes
|
|
105
|
+
fieldSize: 1024 * 1024, // Max field value size (1MB)
|
|
106
|
+
fields: 10, // Max number of non-file fields
|
|
107
|
+
fileSize: 10 * 1024 * 1024, // Max file size (10MB)
|
|
108
|
+
files: 5, // Max number of files
|
|
109
|
+
headerPairs: 2000, // Max number of header pairs
|
|
110
|
+
parts: 1000, // Max number of parts (fields + files)
|
|
111
|
+
},
|
|
112
|
+
// IMPORTANT: Throw on limit exceeded (default is to truncate silently!)
|
|
113
|
+
throwFileSizeLimit: true,
|
|
114
|
+
// Attach all fields to request.body for easier access
|
|
115
|
+
attachFieldsToBody: true,
|
|
116
|
+
// Only accept specific file types (security!)
|
|
117
|
+
// onFile: async (part) => {
|
|
118
|
+
// if (!['image/jpeg', 'image/png'].includes(part.mimetype)) {
|
|
119
|
+
// throw new Error('Invalid file type');
|
|
120
|
+
// }
|
|
121
|
+
// },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Handle file upload
|
|
125
|
+
app.post('/upload', async (request, reply) => {
|
|
126
|
+
const data = await request.file();
|
|
127
|
+
|
|
128
|
+
if (!data) {
|
|
129
|
+
return reply.code(400).send({ error: 'No file uploaded' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// data.file is a stream
|
|
133
|
+
const buffer = await data.toBuffer();
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
filename: data.filename,
|
|
137
|
+
mimetype: data.mimetype,
|
|
138
|
+
size: buffer.length,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Handle multiple files
|
|
143
|
+
app.post('/upload-multiple', async (request) => {
|
|
144
|
+
const files = [];
|
|
145
|
+
|
|
146
|
+
for await (const part of request.files()) {
|
|
147
|
+
const buffer = await part.toBuffer();
|
|
148
|
+
files.push({
|
|
149
|
+
filename: part.filename,
|
|
150
|
+
mimetype: part.mimetype,
|
|
151
|
+
size: buffer.length,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { files };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Handle mixed form data
|
|
159
|
+
app.post('/form', async (request) => {
|
|
160
|
+
const parts = request.parts();
|
|
161
|
+
const fields: Record<string, string> = {};
|
|
162
|
+
const files: Array<{ name: string; size: number }> = [];
|
|
163
|
+
|
|
164
|
+
for await (const part of parts) {
|
|
165
|
+
if (part.type === 'file') {
|
|
166
|
+
const buffer = await part.toBuffer();
|
|
167
|
+
files.push({ name: part.filename, size: buffer.length });
|
|
168
|
+
} else {
|
|
169
|
+
fields[part.fieldname] = part.value as string;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { fields, files };
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Stream Processing
|
|
178
|
+
|
|
179
|
+
Process body as stream for large payloads:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { pipeline } from 'node:stream/promises';
|
|
183
|
+
import { createWriteStream } from 'node:fs';
|
|
184
|
+
|
|
185
|
+
// Add parser that returns stream
|
|
186
|
+
app.addContentTypeParser(
|
|
187
|
+
'application/octet-stream',
|
|
188
|
+
async (request, payload) => {
|
|
189
|
+
return payload; // Return stream directly
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
app.post('/upload-stream', async (request, reply) => {
|
|
194
|
+
const destination = createWriteStream('./upload.bin');
|
|
195
|
+
|
|
196
|
+
await pipeline(request.body, destination);
|
|
197
|
+
|
|
198
|
+
return { success: true };
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Custom JSON Parser
|
|
203
|
+
|
|
204
|
+
Replace the default JSON parser:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Remove default parser
|
|
208
|
+
app.removeContentTypeParser('application/json');
|
|
209
|
+
|
|
210
|
+
// Add custom parser with error handling
|
|
211
|
+
app.addContentTypeParser(
|
|
212
|
+
'application/json',
|
|
213
|
+
{ parseAs: 'string' },
|
|
214
|
+
async (request, body) => {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(body);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw {
|
|
219
|
+
statusCode: 400,
|
|
220
|
+
code: 'INVALID_JSON',
|
|
221
|
+
message: 'Invalid JSON payload',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Content Type with Parameters
|
|
229
|
+
|
|
230
|
+
Handle content types with parameters:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// Match content type with any charset
|
|
234
|
+
app.addContentTypeParser(
|
|
235
|
+
'application/json; charset=utf-8',
|
|
236
|
+
{ parseAs: 'string' },
|
|
237
|
+
async (request, body) => {
|
|
238
|
+
return JSON.parse(body);
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Use regex for flexible matching
|
|
243
|
+
app.addContentTypeParser(
|
|
244
|
+
/^application\/.*\+json$/,
|
|
245
|
+
{ parseAs: 'string' },
|
|
246
|
+
async (request, body) => {
|
|
247
|
+
return JSON.parse(body);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Catch-All Parser
|
|
253
|
+
|
|
254
|
+
Handle unknown content types:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
app.addContentTypeParser('*', async (request, payload) => {
|
|
258
|
+
const chunks: Buffer[] = [];
|
|
259
|
+
|
|
260
|
+
for await (const chunk of payload) {
|
|
261
|
+
chunks.push(chunk);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const buffer = Buffer.concat(chunks);
|
|
265
|
+
|
|
266
|
+
// Try to determine content type
|
|
267
|
+
const contentType = request.headers['content-type'];
|
|
268
|
+
|
|
269
|
+
if (contentType?.includes('json')) {
|
|
270
|
+
return JSON.parse(buffer.toString('utf-8'));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (contentType?.includes('text')) {
|
|
274
|
+
return buffer.toString('utf-8');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return buffer;
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Body Limit Configuration
|
|
282
|
+
|
|
283
|
+
Configure body size limits:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Global limit
|
|
287
|
+
const app = Fastify({
|
|
288
|
+
bodyLimit: 1048576, // 1MB
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Per-route limit
|
|
292
|
+
app.post('/large-upload', {
|
|
293
|
+
bodyLimit: 52428800, // 50MB for this route
|
|
294
|
+
}, async (request) => {
|
|
295
|
+
return { size: JSON.stringify(request.body).length };
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Per content type limit
|
|
299
|
+
app.addContentTypeParser('application/json', {
|
|
300
|
+
parseAs: 'string',
|
|
301
|
+
bodyLimit: 2097152, // 2MB for JSON
|
|
302
|
+
}, async (request, body) => {
|
|
303
|
+
return JSON.parse(body);
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Protocol Buffers
|
|
308
|
+
|
|
309
|
+
Parse protobuf content:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import protobuf from 'protobufjs';
|
|
313
|
+
|
|
314
|
+
const root = await protobuf.load('./schema.proto');
|
|
315
|
+
const MessageType = root.lookupType('package.MessageType');
|
|
316
|
+
|
|
317
|
+
app.addContentTypeParser(
|
|
318
|
+
'application/x-protobuf',
|
|
319
|
+
{ parseAs: 'buffer' },
|
|
320
|
+
async (request, body) => {
|
|
321
|
+
const message = MessageType.decode(body);
|
|
322
|
+
return MessageType.toObject(message);
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Form Data with @fastify/formbody
|
|
328
|
+
|
|
329
|
+
Simple form parsing:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import formbody from '@fastify/formbody';
|
|
333
|
+
|
|
334
|
+
app.register(formbody);
|
|
335
|
+
|
|
336
|
+
app.post('/form', async (request) => {
|
|
337
|
+
// request.body is parsed form data
|
|
338
|
+
const { name, email } = request.body as { name: string; email: string };
|
|
339
|
+
return { name, email };
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Content Negotiation
|
|
344
|
+
|
|
345
|
+
Handle different request formats:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
app.post('/data', async (request, reply) => {
|
|
349
|
+
const contentType = request.headers['content-type'];
|
|
350
|
+
|
|
351
|
+
// Body is already parsed by the appropriate parser
|
|
352
|
+
const data = request.body;
|
|
353
|
+
|
|
354
|
+
// Respond based on Accept header
|
|
355
|
+
const accept = request.headers.accept;
|
|
356
|
+
|
|
357
|
+
if (accept?.includes('application/xml')) {
|
|
358
|
+
reply.type('application/xml');
|
|
359
|
+
return `<data>${JSON.stringify(data)}</data>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
reply.type('application/json');
|
|
363
|
+
return data;
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Validation After Parsing
|
|
368
|
+
|
|
369
|
+
Validate parsed content:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
app.post('/users', {
|
|
373
|
+
schema: {
|
|
374
|
+
body: {
|
|
375
|
+
type: 'object',
|
|
376
|
+
properties: {
|
|
377
|
+
name: { type: 'string', minLength: 1 },
|
|
378
|
+
email: { type: 'string', format: 'email' },
|
|
379
|
+
},
|
|
380
|
+
required: ['name', 'email'],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
}, async (request) => {
|
|
384
|
+
// Body is parsed AND validated
|
|
385
|
+
return request.body;
|
|
386
|
+
});
|
|
387
|
+
```
|