@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,423 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deployment
|
|
3
|
+
description: Production deployment for Fastify applications
|
|
4
|
+
metadata:
|
|
5
|
+
tags: deployment, production, docker, kubernetes, scaling
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Production Deployment
|
|
9
|
+
|
|
10
|
+
## Graceful Shutdown with close-with-grace
|
|
11
|
+
|
|
12
|
+
Use `close-with-grace` for proper shutdown handling:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import closeWithGrace from 'close-with-grace';
|
|
17
|
+
|
|
18
|
+
const app = Fastify({ logger: true });
|
|
19
|
+
|
|
20
|
+
// Register plugins and routes
|
|
21
|
+
await app.register(import('./plugins/index.js'));
|
|
22
|
+
await app.register(import('./routes/index.js'));
|
|
23
|
+
|
|
24
|
+
// Graceful shutdown handler
|
|
25
|
+
closeWithGrace({ delay: 10000 }, async ({ signal, err }) => {
|
|
26
|
+
if (err) {
|
|
27
|
+
app.log.error({ err }, 'Server closing due to error');
|
|
28
|
+
} else {
|
|
29
|
+
app.log.info({ signal }, 'Server closing due to signal');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await app.close();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Start server
|
|
36
|
+
await app.listen({
|
|
37
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
38
|
+
host: '0.0.0.0',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.log.info(`Server listening on ${app.server.address()}`);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Health Check Endpoints
|
|
45
|
+
|
|
46
|
+
Implement comprehensive health checks:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
app.get('/health', async () => {
|
|
50
|
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
app.get('/health/live', async () => {
|
|
54
|
+
return { status: 'ok' };
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.get('/health/ready', async (request, reply) => {
|
|
58
|
+
const checks = {
|
|
59
|
+
database: false,
|
|
60
|
+
cache: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await app.db`SELECT 1`;
|
|
65
|
+
checks.database = true;
|
|
66
|
+
} catch {
|
|
67
|
+
// Database not ready
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await app.cache.ping();
|
|
72
|
+
checks.cache = true;
|
|
73
|
+
} catch {
|
|
74
|
+
// Cache not ready
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const allHealthy = Object.values(checks).every(Boolean);
|
|
78
|
+
|
|
79
|
+
if (!allHealthy) {
|
|
80
|
+
reply.code(503);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
status: allHealthy ? 'ok' : 'degraded',
|
|
85
|
+
checks,
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Detailed health for monitoring
|
|
91
|
+
app.get('/health/details', {
|
|
92
|
+
preHandler: [app.authenticate, app.requireAdmin],
|
|
93
|
+
}, async () => {
|
|
94
|
+
const memory = process.memoryUsage();
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
status: 'ok',
|
|
98
|
+
uptime: process.uptime(),
|
|
99
|
+
memory: {
|
|
100
|
+
heapUsed: Math.round(memory.heapUsed / 1024 / 1024),
|
|
101
|
+
heapTotal: Math.round(memory.heapTotal / 1024 / 1024),
|
|
102
|
+
rss: Math.round(memory.rss / 1024 / 1024),
|
|
103
|
+
},
|
|
104
|
+
version: process.env.APP_VERSION,
|
|
105
|
+
nodeVersion: process.version,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
## Docker Configuration
|
|
110
|
+
|
|
111
|
+
Create an optimized Dockerfile:
|
|
112
|
+
|
|
113
|
+
```dockerfile
|
|
114
|
+
# Build stage
|
|
115
|
+
FROM node:22-alpine AS builder
|
|
116
|
+
|
|
117
|
+
WORKDIR /app
|
|
118
|
+
|
|
119
|
+
COPY package*.json ./
|
|
120
|
+
RUN npm ci --only=production
|
|
121
|
+
|
|
122
|
+
COPY . .
|
|
123
|
+
|
|
124
|
+
# Production stage
|
|
125
|
+
FROM node:22-alpine
|
|
126
|
+
|
|
127
|
+
WORKDIR /app
|
|
128
|
+
|
|
129
|
+
# Run as non-root user
|
|
130
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
131
|
+
adduser -S nodejs -u 1001
|
|
132
|
+
|
|
133
|
+
# Copy from builder
|
|
134
|
+
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
|
|
135
|
+
COPY --from=builder --chown=nodejs:nodejs /app/src ./src
|
|
136
|
+
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
|
|
137
|
+
|
|
138
|
+
USER nodejs
|
|
139
|
+
|
|
140
|
+
EXPOSE 3000
|
|
141
|
+
|
|
142
|
+
ENV NODE_ENV=production
|
|
143
|
+
ENV PORT=3000
|
|
144
|
+
|
|
145
|
+
# Health check
|
|
146
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
147
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
148
|
+
|
|
149
|
+
CMD ["node", "src/app.ts"]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
# docker-compose.yml
|
|
154
|
+
services:
|
|
155
|
+
api:
|
|
156
|
+
build: .
|
|
157
|
+
ports:
|
|
158
|
+
- "3000:3000"
|
|
159
|
+
environment:
|
|
160
|
+
- NODE_ENV=production
|
|
161
|
+
- DATABASE_URL=postgres://user:pass@db:5432/app
|
|
162
|
+
- JWT_SECRET=${JWT_SECRET}
|
|
163
|
+
depends_on:
|
|
164
|
+
db:
|
|
165
|
+
condition: service_healthy
|
|
166
|
+
restart: unless-stopped
|
|
167
|
+
|
|
168
|
+
db:
|
|
169
|
+
image: postgres:16-alpine
|
|
170
|
+
environment:
|
|
171
|
+
- POSTGRES_USER=user
|
|
172
|
+
- POSTGRES_PASSWORD=pass
|
|
173
|
+
- POSTGRES_DB=app
|
|
174
|
+
volumes:
|
|
175
|
+
- pgdata:/var/lib/postgresql/data
|
|
176
|
+
healthcheck:
|
|
177
|
+
test: ["CMD-SHELL", "pg_isready -U user -d app"]
|
|
178
|
+
interval: 5s
|
|
179
|
+
timeout: 5s
|
|
180
|
+
retries: 5
|
|
181
|
+
|
|
182
|
+
volumes:
|
|
183
|
+
pgdata:
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Kubernetes Deployment
|
|
187
|
+
|
|
188
|
+
Deploy to Kubernetes:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
# deployment.yaml
|
|
192
|
+
apiVersion: apps/v1
|
|
193
|
+
kind: Deployment
|
|
194
|
+
metadata:
|
|
195
|
+
name: fastify-api
|
|
196
|
+
spec:
|
|
197
|
+
replicas: 3
|
|
198
|
+
selector:
|
|
199
|
+
matchLabels:
|
|
200
|
+
app: fastify-api
|
|
201
|
+
template:
|
|
202
|
+
metadata:
|
|
203
|
+
labels:
|
|
204
|
+
app: fastify-api
|
|
205
|
+
spec:
|
|
206
|
+
containers:
|
|
207
|
+
- name: api
|
|
208
|
+
image: my-registry/fastify-api:latest
|
|
209
|
+
ports:
|
|
210
|
+
- containerPort: 3000
|
|
211
|
+
env:
|
|
212
|
+
- name: NODE_ENV
|
|
213
|
+
value: "production"
|
|
214
|
+
- name: DATABASE_URL
|
|
215
|
+
valueFrom:
|
|
216
|
+
secretKeyRef:
|
|
217
|
+
name: api-secrets
|
|
218
|
+
key: database-url
|
|
219
|
+
resources:
|
|
220
|
+
requests:
|
|
221
|
+
memory: "256Mi"
|
|
222
|
+
cpu: "100m"
|
|
223
|
+
limits:
|
|
224
|
+
memory: "512Mi"
|
|
225
|
+
cpu: "500m"
|
|
226
|
+
livenessProbe:
|
|
227
|
+
httpGet:
|
|
228
|
+
path: /health/live
|
|
229
|
+
port: 3000
|
|
230
|
+
initialDelaySeconds: 5
|
|
231
|
+
periodSeconds: 10
|
|
232
|
+
readinessProbe:
|
|
233
|
+
httpGet:
|
|
234
|
+
path: /health/ready
|
|
235
|
+
port: 3000
|
|
236
|
+
initialDelaySeconds: 5
|
|
237
|
+
periodSeconds: 5
|
|
238
|
+
lifecycle:
|
|
239
|
+
preStop:
|
|
240
|
+
exec:
|
|
241
|
+
command: ["/bin/sh", "-c", "sleep 5"]
|
|
242
|
+
---
|
|
243
|
+
apiVersion: v1
|
|
244
|
+
kind: Service
|
|
245
|
+
metadata:
|
|
246
|
+
name: fastify-api
|
|
247
|
+
spec:
|
|
248
|
+
selector:
|
|
249
|
+
app: fastify-api
|
|
250
|
+
ports:
|
|
251
|
+
- port: 80
|
|
252
|
+
targetPort: 3000
|
|
253
|
+
type: ClusterIP
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Production Logger Configuration
|
|
257
|
+
|
|
258
|
+
Configure logging for production:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import Fastify from 'fastify';
|
|
262
|
+
|
|
263
|
+
const app = Fastify({
|
|
264
|
+
logger: {
|
|
265
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
266
|
+
// JSON output for log aggregation
|
|
267
|
+
formatters: {
|
|
268
|
+
level: (label) => ({ level: label }),
|
|
269
|
+
bindings: (bindings) => ({
|
|
270
|
+
pid: bindings.pid,
|
|
271
|
+
hostname: bindings.hostname,
|
|
272
|
+
service: 'fastify-api',
|
|
273
|
+
version: process.env.APP_VERSION,
|
|
274
|
+
}),
|
|
275
|
+
},
|
|
276
|
+
timestamp: () => `,"time":"${new Date().toISOString()}"`,
|
|
277
|
+
// Redact sensitive data
|
|
278
|
+
redact: {
|
|
279
|
+
paths: [
|
|
280
|
+
'req.headers.authorization',
|
|
281
|
+
'req.headers.cookie',
|
|
282
|
+
'*.password',
|
|
283
|
+
'*.token',
|
|
284
|
+
'*.secret',
|
|
285
|
+
],
|
|
286
|
+
censor: '[REDACTED]',
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Request Timeouts
|
|
293
|
+
|
|
294
|
+
Configure appropriate timeouts:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const app = Fastify({
|
|
298
|
+
connectionTimeout: 30000, // 30s connection timeout
|
|
299
|
+
keepAliveTimeout: 72000, // 72s keep-alive (longer than ALB 60s)
|
|
300
|
+
requestTimeout: 30000, // 30s request timeout
|
|
301
|
+
bodyLimit: 1048576, // 1MB body limit
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Per-route timeout
|
|
305
|
+
app.get('/long-operation', {
|
|
306
|
+
config: {
|
|
307
|
+
timeout: 60000, // 60s for this route
|
|
308
|
+
},
|
|
309
|
+
}, longOperationHandler);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Trust Proxy Settings
|
|
313
|
+
|
|
314
|
+
Configure for load balancers:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const app = Fastify({
|
|
318
|
+
// Trust first proxy (load balancer)
|
|
319
|
+
trustProxy: true,
|
|
320
|
+
|
|
321
|
+
// Or trust specific proxies
|
|
322
|
+
trustProxy: ['127.0.0.1', '10.0.0.0/8'],
|
|
323
|
+
|
|
324
|
+
// Or number of proxies to trust
|
|
325
|
+
trustProxy: 1,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Now request.ip returns real client IP
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Static File Serving
|
|
332
|
+
|
|
333
|
+
Serve static files efficiently. **Always use `import.meta.dirname` as the base path**, never `process.cwd()`:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import fastifyStatic from '@fastify/static';
|
|
337
|
+
import { join } from 'node:path';
|
|
338
|
+
|
|
339
|
+
app.register(fastifyStatic, {
|
|
340
|
+
root: join(import.meta.dirname, '..', 'public'),
|
|
341
|
+
prefix: '/static/',
|
|
342
|
+
maxAge: '1d',
|
|
343
|
+
immutable: true,
|
|
344
|
+
etag: true,
|
|
345
|
+
lastModified: true,
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Compression
|
|
350
|
+
|
|
351
|
+
Enable response compression:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import fastifyCompress from '@fastify/compress';
|
|
355
|
+
|
|
356
|
+
app.register(fastifyCompress, {
|
|
357
|
+
global: true,
|
|
358
|
+
threshold: 1024, // Only compress > 1KB
|
|
359
|
+
encodings: ['gzip', 'deflate'],
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Metrics and Monitoring
|
|
364
|
+
|
|
365
|
+
Expose Prometheus metrics:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client';
|
|
369
|
+
|
|
370
|
+
collectDefaultMetrics();
|
|
371
|
+
|
|
372
|
+
const httpRequestDuration = new Histogram({
|
|
373
|
+
name: 'http_request_duration_seconds',
|
|
374
|
+
help: 'Duration of HTTP requests in seconds',
|
|
375
|
+
labelNames: ['method', 'route', 'status'],
|
|
376
|
+
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const httpRequestTotal = new Counter({
|
|
380
|
+
name: 'http_requests_total',
|
|
381
|
+
help: 'Total number of HTTP requests',
|
|
382
|
+
labelNames: ['method', 'route', 'status'],
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
app.addHook('onResponse', (request, reply, done) => {
|
|
386
|
+
const route = request.routeOptions.url || request.url;
|
|
387
|
+
const labels = {
|
|
388
|
+
method: request.method,
|
|
389
|
+
route,
|
|
390
|
+
status: reply.statusCode,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
httpRequestDuration.observe(labels, reply.elapsedTime / 1000);
|
|
394
|
+
httpRequestTotal.inc(labels);
|
|
395
|
+
done();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
app.get('/metrics', async (request, reply) => {
|
|
399
|
+
reply.header('Content-Type', register.contentType);
|
|
400
|
+
return register.metrics();
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Zero-Downtime Deployments
|
|
405
|
+
|
|
406
|
+
Support rolling updates:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import closeWithGrace from 'close-with-grace';
|
|
410
|
+
|
|
411
|
+
// Stop accepting new connections gracefully
|
|
412
|
+
closeWithGrace({ delay: 30000 }, async ({ signal }) => {
|
|
413
|
+
app.log.info({ signal }, 'Received shutdown signal');
|
|
414
|
+
|
|
415
|
+
// Stop accepting new connections
|
|
416
|
+
// Existing connections continue to be served
|
|
417
|
+
|
|
418
|
+
// Wait for in-flight requests (handled by close-with-grace delay)
|
|
419
|
+
await app.close();
|
|
420
|
+
|
|
421
|
+
app.log.info('Server closed');
|
|
422
|
+
});
|
|
423
|
+
```
|