L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

nodejs-fastify

nodejs-fastify

Use this skill when building Fastify applications — schema validation, plugin system, hooks, serialization. This skill enforces: JSON Schema validation, plugin encapsulation, schema serializers, Fastify lifecycle hooks. Do NOT use for: Express.js apps, database schema, frontend, or non-Fastify Node backends.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: nodejs-fastify description: > Use this skill when building Fastify applications — schema validation, plugin system, hooks, serialization. This skill enforces: JSON Schema validation, plugin encapsulation, schema serializers, Fastify lifecycle hooks. Do NOT use for: Express.js apps, database schema, frontend, or non-Fastify Node backends. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, nodejs, fastify, phase-10]

Node.js Fastify

Purpose

Build Fastify applications with schema-first validation, plugin encapsulation, request/response serialization, and lifecycle hooks.

Agent Protocol

Trigger

User request includes: fastify, fastify server, fastify plugin, fastify schema, fastify hooks, fastify validation, fastify serializer, TypeBox, @fastify/swagger.

Input Context

  • Fastify version (4.x, 5.x)
  • Validation approach (JSON Schema, TypeBox, Zod)
  • Plugin set (swagger, cors, jwt, rate-limit)
  • Database (TypeORM, Prisma, Knex)

Output Artifact

Server setup, plugin registration, schema definitions, hook pipeline, route patterns.

Response Format

Produce artifact directly. No preamble, no postamble, no explanations.

Completion Criteria

  • Server created with logger and schema validator
  • Plugins registered with encapsulation
  • Routes defined with JSON Schema validation
  • Hooks attached at correct lifecycle points
  • Swagger/OpenAPI documentation configured

Max Response Length

4096 tokens

Architecture Decision Trees

Validation: JSON Schema vs TypeBox vs Zod

CriterionJSON SchemaTypeBoxZod
Type inferenceManualStatic<typeof schema>z.infer<typeof schema>
Code-firstNoYesYes
Composition$refType.Union, Type.Intersectz.union, z.intersection
PerformanceFast native compilationFastest (TypeCode compiler)Good
EcosystemStandard, swagger compatibleFastify-nativeWidely used

Decision: Need OpenAPI/Swagger docs → JSON Schema or TypeBox. Want maximum type safety → TypeBox. Already using Zod in codebase → Zod.

Plugin vs Register Scope

ScopeEncapsulationUse Case
RootGlobalcors, rate-limiter, jwt, formbody
Plugin-levelEncapsulated within pluginFeature modules, route groups
ChildNested encapsulationSub-plugins within feature

Decision: Infrastructure cross-cutting → Root. Feature-specific → Plugin. Shared helpers → fastify-plugin (bypass encapsulation).

Workflow

Step 1: Server Bootstrap

// src/server.ts
import Fastify from 'fastify';
import { registerPlugins } from './plugins';
import { config } from './config';

export async function buildServer() {
  const server = Fastify({
    logger: {
      level: config.LOG_LEVEL,
      transport: config.NODE_ENV === 'development'
        ? { target: 'pino-pretty' }
        : undefined,
    },
    ajv: {
      customOptions: { allErrors: true, coerceTypes: 'array' },
    },
  });

  await registerPlugins(server);

  // Health check
  server.get('/health', async () => ({ status: 'ok', uptime: process.uptime() }));

  return server;
}

// src/index.ts
import { buildServer } from './server';

async function main() {
  const server = await buildServer();
  const port = parseInt(process.env.PORT || '3000');

  await server.listen({ port, host: '0.0.0.0' });

  const shutdown = async (signal: string) => {
    server.log.info(`${signal} received. Shutting down...`);
    await server.close();
    process.exit(0);
  };
  process.on('SIGTERM', () => shutdown('SIGTERM'));
  process.on('SIGINT', () => shutdown('SIGINT'));
}

main().catch(err => {
  console.error('Fatal:', err);
  process.exit(1);
});

Step 2: Plugin Registration

// src/plugins/index.ts
import { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import rateLimit from '@fastify/rate-limit';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import jwt from '@fastify/jwt';
import { userModule } from '../modules/users';
import { orderModule } from '../modules/orders';
import { errorHandlerPlugin } from './error-handler';
import { config } from '../config';

export async function registerPlugins(app: FastifyInstance) {
  // Core plugins (global)
  await app.register(cors, { origin: config.CORS_ORIGIN });
  await app.register(helmet);
  await app.register(rateLimit, { max: 100, timeWindow: '1 minute' });
  await app.register(jwt, { secret: config.JWT_SECRET });

  // Documentation
  await app.register(swagger, {
    openapi: { info: { title: 'API', version: '1.0.0' } },
  });
  await app.register(swaggerUi, { routePrefix: '/docs' });

  // Error handler
  await app.register(errorHandlerPlugin);

  // Feature modules (encapsulated)
  await app.register(userModule, { prefix: '/api/v1/users' });
  await app.register(orderModule, { prefix: '/api/v1/orders' });
}

Step 3: Schema-First Routes with TypeBox

// src/modules/users/schema.ts
import { Type, Static } from '@sinclair/typebox';

export const CreateUserSchema = Type.Object({
  name: Type.String({ minLength: 2, maxLength: 100 }),
  email: Type.String({ format: 'email' }),
  role: Type.Optional(Type.Union([Type.Literal('admin'), Type.Literal('user')])),
});

export const UserResponseSchema = Type.Object({
  id: Type.String({ format: 'uuid' }),
  name: Type.String(),
  email: Type.String({ format: 'email' }),
  role: Type.String(),
  createdAt: Type.String({ format: 'date-time' }),
});

export const UserParamsSchema = Type.Object({
  id: Type.String({ format: 'uuid' }),
});

export type CreateUserDto = Static<typeof CreateUserSchema>;

// src/modules/users/routes.ts
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { userService } from './service';
import { CreateUserSchema, UserResponseSchema, UserParamsSchema } from './schema';

export async function userRoutes(app: FastifyInstance) {
  app.get('/', {
    schema: {
      querystring: Type.Object({
        page: Type.Optional(Type.Number({ default: 1 })),
        limit: Type.Optional(Type.Number({ default: 20 })),
      }),
      response: {
        200: Type.Object({
          data: Type.Array(UserResponseSchema),
          total: Type.Number(),
          page: Type.Number(),
        }),
      },
    },
    handler: async (request, reply) => {
      const { page, limit } = request.query;
      const result = await userService.findAll(page, limit);
      return result;
    },
  });

  app.get<{ Params: Static<typeof UserParamsSchema> }>('/:id', {
    schema: {
      params: UserParamsSchema,
      response: { 200: UserResponseSchema, 404: Type.Object({ error: Type.String() }) },
    },
    handler: async (request, reply) => {
      const user = await userService.findById(request.params.id);
      if (!user) {
        reply.code(404);
        return { error: 'User not found' };
      }
      return user;
    },
  });

  app.post('/', {
    schema: {
      body: CreateUserSchema,
      response: { 201: UserResponseSchema },
    },
    handler: async (request, reply) => {
      const user = await userService.create(request.body);
      reply.code(201);
      return user;
    },
  });
}

Step 4: Hooks Lifecycle

// src/plugins/hooks.ts
import { FastifyInstance } from 'fastify';

export async function registerHooks(app: FastifyInstance) {
  // Pre-validation — transform request
  app.addHook('preValidation', async (request, reply) => {
    // e.g., add request ID
    request.id = request.id || crypto.randomUUID();
  });

  // Pre-handler — auth check
  app.addHook('preHandler', async (request, reply) => {
    if (request.url.startsWith('/api/v1/auth')) return;
    try {
      await request.jwtVerify();
    } catch (err) {
      reply.code(401).send({ error: 'Unauthorized' });
    }
  });

  // On-send — response logging
  app.addHook('onSend', async (request, reply, payload) => {
    reply.header('X-Request-Id', request.id as string);
    return payload;
  });

  // On-response — metrics
  app.addHook('onResponse', async (request, reply) => {
    const elapsed = reply.elapsedTime;
    app.log.info({ method: request.method, url: request.url, status: reply.statusCode, elapsed }, 'response');
  });
}

Step 5: Error Handler Plugin

// src/plugins/error-handler.ts
import { FastifyInstance, FastifyError, FastifyReply, FastifyRequest } from 'fastify';

export async function errorHandlerPlugin(app: FastifyInstance) {
  app.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
    const statusCode = error.statusCode || 500;

    app.log.error({ err: error, requestId: request.id }, error.message);

    if (statusCode === 429) {
      return reply.code(429).send({ error: 'Too many requests', retryAfter: error.message });
    }

    if (error.validation) {
      return reply.code(400).send({
        error: 'Validation Error',
        details: error.validation.map(v => ({
          field: v.instancePath,
          message: v.message,
        })),
      });
    }

    const response = statusCode >= 500
      ? { error: 'Internal Server Error' }
      : { error: error.message };

    return reply.code(statusCode).send(response);
  });
}

Step 6: Graceful Shutdown with Health Check

// Graceful shutdown wrapper
async function main() {
  const app = await buildServer();
  await app.listen({ port: 3000 });

  const signals = ['SIGTERM', 'SIGINT'];
  for (const signal of signals) {
    process.on(signal, async () => {
      app.log.info(`${signal} received, shutting down`);
      await app.close();
      process.exit(0);
    });
  }
}

Implementation Patterns

Pattern: Encapsulated Plugin Module

// src/modules/users/index.ts
import { FastifyInstance } from 'fastify';
import { userRoutes } from './routes';

export async function userModule(app: FastifyInstance) {
  // Auth decorator (encapsulated — only within this plugin)
  app.decorate('userModule', true);

  // Pre-handler specific to this module
  app.addHook('preHandler', async (request, reply) => {
    // Module-level auth
  });

  await app.register(userRoutes);
}

Pattern: Custom Decorator

// src/plugins/auth-decorators.ts
import { FastifyInstance, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';

declare module 'fastify' {
  interface FastifyInstance {
    authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
  }
}

export default fp(async function (app: FastifyInstance) {
  app.decorate('authenticate', async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      await request.jwtVerify();
    } catch (err) {
      reply.code(401).send({ error: 'Unauthorized' });
    }
  });
});

Production Considerations

Serialization

Fastify serializes responses using compiled schemas. Define response schemas for maximum performance. Use @fastify/response-validation for development response validation.

Compression

await app.register(import('@fastify/compress'), { global: true, threshold: 1024 });

Trust Proxy

const server = Fastify({ trustProxy: true });

Anti-Patterns

Anti-PatternWhyFix
No schema validationManual validation, no serialization benefitsAlways define schema on routes
Global plugins modifying requestEncapsulation violationUse fastify-plugin for shared decorators
Reply payload manual JSON.stringifyBreaks serialization optimizationReturn object, let Fastify serialize
Heavy imports in hot handlerSlows non-related requestsUse await import() or separate plugins
Async route without error handlingUnhandled promise rejectionFastify wraps async handlers automatically

Security Considerations

  • @fastify/helmet for security headers — register early
  • @fastify/cors with explicit origins — never * with credentials
  • @fastify/rate-limit per IP — configure per route for auth endpoints
  • @fastify/csrf protection for cookie-based auth
  • Schema validation strips unknown properties by default (additionalProperties: false)
  • JWT via @fastify/jwt with cookie option for cookie-based auth

Testing Strategies

import { buildServer } from '../src/server';
import { test, expect } from 'vitest';

test('POST /api/v1/users creates user', async () => {
  const app = await buildServer();
  const response = await app.inject({
    method: 'POST',
    url: '/api/v1/users',
    payload: { name: 'Test', email: 'test@test.com' },
  });
  expect(response.statusCode).toBe(201);
  expect(response.json().data).toHaveProperty('id');
});

test('GET /api/v1/users/:id returns 404', async () => {
  const app = await buildServer();
  const response = await app.inject({
    method: 'GET',
    url: '/api/v1/users/nonexistent',
  });
  expect(response.statusCode).toBe(404);
});

Use app.inject() for HTTP testing without a server. Use @fastify/test-helper for lifecycle tests.

Rules

  • Every route has a schema with body/querystring/params/response definitions.
  • Schema-first approach with TypeBox for type inference.
  • Plugin encapsulation: feature modules register their own routes, hooks, decorators.
  • Never use require — always import. Fastify supports native ESM.
  • Lifecycle hooks: onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse.
  • @fastify/swagger generates OpenAPI docs from route schemas automatically.
  • Graceful shutdown via app.close() in signal handlers.

References

  • references/fastify-advanced-plugins.md — Advanced Plugin Patterns
  • references/fastify-hooks.md — Lifecycle Hooks
  • references/fastify-plugins.md — Plugin System
  • references/fastify-setup.md — Setup Guide
  • references/fastify-testing.md — Testing
  • references/fastify-validation.md — Validation and Serialization

Handoff

Hand off to backend/universal/api-response/SKILL.md for API response formatting or backend/nodejs/patterns/SKILL.md for Node.js patterns.

Implementation Patterns

Factory Pattern for Module Creation

function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }

Builder Pattern for Complex Configuration

class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }

Production Considerations

Deployment Checklist

  • Production build with optimizations enabled
  • Environment variables configured per environment
  • Health check endpoint responds correctly
  • Error tracking and monitoring integrated
  • Logging level configured (not debug in production)
  • Resource limits configured
  • Database migrations applied
  • Static assets built and served from CDN or cache
  • Feature flags toggled appropriately
  • Rollback plan documented and tested

Monitoring and Alerting

MetricThresholdSeverityAction
Error rate> 1%CriticalRollback or fix
p95 latency> 500msWarningProfile and optimize
Uptime< 99.9%CriticalInvestigate infrastructure
Memory usage> 80%WarningCheck for leaks
CPU usage> 80%WarningScale up or optimize

Rules

  • Prefer composition over inheritance
  • Favor immutable data structures
  • Use dependency injection for testability
  • Keep functions pure when possible — no side effects
  • Fail fast with clear error messages
  • Don't repeat yourself (DRY) — extract shared logic
  • Keep it simple (KISS) — avoid unnecessary complexity
  • You aren't gonna need it (YAGNI) — build what's required
  • Separate concerns — single responsibility per module
  • Code to interfaces, not implementations
  • Write self-documenting code — clear names over comments
  • Prefer standard library over third-party dependencies
  • Handle errors explicitly — no silent failures
  • Validate inputs at boundaries
  • Log at appropriate levels (debug, info, warn, error)
Skills enregistrées