Node.js Framework

Fastify

Fastify es un framework web para Node.js rápido y basado en esquemas (schema-first). JSON Schema compila tu validación y serialización, mientras que los plugins encapsulados mantienen la integridad de los servicios a gran escala.

intermediate15 min readUpdated 16 sept 2026
server.ts
ts
// server.ts
import Fastify from "fastify";
import { Type } from "@sinclair/typebox";

const app = Fastify({ logger: true });

const User = Type.Object({
  id: Type.String({ format: "uuid" }),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: "email" }),
});

app.get("/users/:id", {
  schema: {
    params: Type.Object({ id: Type.String({ format: "uuid" }) }),
    response: { 200: User },
  },
}, async (request, reply) => {
  const user = await db.users.findById(request.params.id);
  if (!user) return reply.callNotFound();
  return user;
});

await app.listen({ port: 3000, host: "0.0.0.0" });
Lanzado
2016
Ejecuta en
Node.js
Estilo
Schema-first, basado en plugins
Idea central
JSON Schema
Lenguaje
JavaScript / TypeScript
Versión
5.x

Por que importa

Por qué Fastify gana en rendimiento (throughput)

Rendimiento por diseño

Un router de árbol radix y esquemas compilados permiten que Fastify gestione muchas más solicitudes por segundo que Express, manteniendo una API familiar.

Validación que se compila

Un JSON Schema no es solo documentación. Fastify lo compila en un validador y un serializador, por lo que las entradas incorrectas se rechazan antes de que se ejecute tu handler.

Plugins encapsulados

Cada plugin tiene su propio scope. Los decoradores y hooks permanecen locales a menos que los compartas explícitamente, evitando que los servicios grandes filtren el estado.

La imagen completa

Las tres ideas detrás de Fastify

Declara la forma de tus datos, envuelve las funcionalidades en plugins encapsulados e intercepta la solicitud en puntos definidos del ciclo de vida.

JSON Schema

Declarar

Describe los params, body, query y respuestas una sola vez, y Fastify se encarga de validarlos y serializarlos por ti.

Plugins

Encapsular

Un plugin es una función que puede decorar la instancia y registrar rutas dentro de su propio scope.

Hooks

Interceptar

Los hooks del ciclo de vida como onRequest, preHandler y onSend se ejecutan en puntos precisos de cada solicitud.

La guia completa

Fastify: Todo lo que necesitas saber

¿Qué es Fastify?

Fastify es un framework web para Node.js construido bajo una premisa: si describes tus datos, el framework puede encargarse de gran parte del trabajo. Combina un router extremadamente rápido con un sistema de esquemas, un modelo de plugins con alcance definido (scoped) y un ciclo de vida basado en hooks. El resultado es un framework que es rápido y tiene una postura clara sobre la seguridad, sin imponerte una estructura de carpetas.

Surgió en 2016 y alcanzó la versión estable 1.0 en 2018. Hoy en día, impulsa APIs en producción de empresas que necesitan una ergonomía similar a la de Express, pero con un rendimiento notablemente superior y un sistema de validación real. Si conoces Express, la mayor parte de Fastify te resultará familiar en cuestión de una tarde.

Schema-first: validación que se compila

La característica definitoria es que los schemas son ejecutables. Cuando asocias un JSON Schema a una ruta, Fastify lo compila una sola vez al iniciar y utiliza el validador compilado para cada solicitud. No ocurre ninguna interpretación por llamada, razón por la cual la validación es prácticamente gratuita.

import { Type, type Static } from "@sinclair/typebox";

const CreateUser = Type.Object({
  name: Type.String({ minLength: 1, maxLength: 80 }),
  email: Type.String({ format: "email" }),
});

type CreateUser = Static<typeof CreateUser>;

app.post("/users", {
  schema: { body: CreateUser },
}, async (request) => {
  // request.body is validated and typed as CreateUser
  return createUser(request.body);
});

Esto ofrece dos beneficios. Primero, las solicitudes inválidas son rechazadas con un 400 antes de que se llame a tu handler, por lo que la lógica de negocio solo recibe datos limpios. Segundo, el mismo schema impulsa la serialización: Fastify compila el schema de respuesta en una función fast-json-stringify, que es drásticamente más rápida que JSON.stringify y garantiza que nunca filtres campos que no hayas declarado.

Rutas y opciones de ruta

Una ruta es un método, una ruta (path) y un manejador (handler), pero la parte interesante es el objeto de opciones que se encuentra entre ellos. Aquí es donde residen el esquema, el manejador y los metadatos específicos de cada ruta.

app.route({
  method: "GET",
  url: "/health",
  config: { public: true },
  schema: {
    response: {
      200: Type.Object({ status: Type.String() }),
    },
  },
  handler: async () => ({ status: "ok" }),
});

Los métodos abreviados (app.get, app.post y así sucesivamente) aceptan las mismas opciones como segundo argumento. Los parámetros de la ruta llegan en request.params, la cadena de consulta (query string) en request.query y el cuerpo analizado (parsed body) en request.body. Debido a que las estructuras provienen del esquema, TypeScript reconoce los tres.

reply es la otra mitad del manejador. reply.code(404), reply.header(...) y reply.send(...) funcionan de manera similar a Express, y devolver un valor desde un manejador async es una forma abreviada de hacer reply.send. Fastify también incluye ayudantes como reply.callNotFound() para que los flujos de error se mantengan consistentes.

Plugins y encapsulamiento

Fastify no tiene una cadena de middleware en el sentido de Express. En su lugar, todo es un plugin, y cada plugin tiene su propio scope. Esa única regla es lo que hace que las aplicaciones grandes sean predecibles.

import fp from "fastify-plugin";

const dbPlugin = fp(async (app) => {
  app.decorate("users", createUserRepository(app.log));
}, { name: "db" });

await app.register(dbPlugin);

Sin fastify-plugin, los decorators y hooks añadidos dentro del plugin serían visibles únicamente para las rutas registradas dentro de ese mismo plugin. Eso es el encapsulamiento: una funcionalidad puede poseer sus propias dependencias y configuración sin contaminar el resto de la app. Envolver con fp elimina deliberadamente esa frontera cuando estás construyendo infraestructura compartida, como una base de datos o un logger.

Los decorators son la forma idiomática de adjuntar funcionalidad: app.decorate("users", repo) para la instancia, app.decorateRequest("user", null) para la request y app.decorateReply para la reply. Debido a que están tipados mediante module augmentation, obtienes autocompletado en lugar de any.

El ciclo de vida: hooks en orden

Los hooks te permiten ejecutar código en puntos definidos sin necesidad de envolver los handlers. Se ejecutan en un orden fijo:

  1. onRequest — el punto más temprano, ideal para autenticación e IDs de solicitud.
  2. preParsing — antes de que se lea el cuerpo, para compresión o comprobaciones de tamaño.
  3. preValidation — después del parsing, antes de la validación del esquema.
  4. preHandler — después de la validación, justo antes del handler.
  5. preSerialization y onSend — dan forma al payload a la salida.
  6. onResponse y onError — observan la solicitud finalizada.
app.addHook("onRequest", async (request) => {
  request.start = process.hrtime.bigint();
});

app.addHook("onResponse", async (request, reply) => {
  const ms = Number(process.hrtime.bigint() - request.start) / 1e6;
  request.log.info({ ms }, "request completed");
});

Los hooks tienen un alcance similar al de los plugins, por lo que un hook añadido dentro de un plugin solo se ejecuta para las rutas de ese plugin. onClose es la contraparte para el apagado, y es donde liberas los pools de bases de datos y los timers. Dominar el orden es la habilidad principal; la documentación lo detalla con precisión y rara vez cambia.

TypeScript sin complicaciones

Fastify está escrito en TypeScript y sus tipos son prioritarios. Defines el tipo de una ruta pasando parámetros genéricos, y los errores de validación aparecen en tiempo de compilación en lugar de en producción.

app.get<{
  Params: { id: string };
  Querystring: { fields?: string };
}>("/users/:id", async (request) => {
  const { id } = request.params;
  const { fields } = request.query;
  return findUser(id, fields);
});

El patrón más valioso son los type providers. Con @fastify/type-provider-typebox, el esquema mismo se convierte en el tipo, por lo que nunca tienes que escribir la interfaz dos veces y evitas que ambas se desincronicen.

import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

const app = Fastify().withTypeProvider<TypeBoxTypeProvider>();

app.post("/users", { schema: { body: CreateUser } }, async (request) => {
  return createUser(request.body); // typed as CreateUser
});

La aumentación de módulos (module augmentation) es la forma en que los decoradores obtienen sus tipos: declaras la propiedad en FastifyInstance o FastifyRequest, y estará disponible en todas partes. Cuando algo aparece como unknown, suele ser una señal de que falta un esquema o una declaración.

Logging con pino, integrado

Fastify incluye pino como su logger. Actívalo con una sola opción y cada solicitud generará una línea de log en JSON estructurado con un id, tiempos de ejecución y tus propios campos.

const app = Fastify({
  logger: {
    level: "info",
    redact: ["req.headers.authorization"],
  },
});

app.get("/orders", async (request) => {
  request.log.info({ userId: request.user?.id }, "listing orders");
  return listOrders(request.user?.id);
});

Al ser pino, la salida es JSON delimitado por saltos de línea que se envía limpiamente a cualquier agregador de logs, y request.log incluye automáticamente el contexto de la solicitud. En desarrollo, redirige la salida a través de pino-pretty para obtener un formato legible; en producción, mantén el JSON. Las reglas de redacción son la forma más segura de evitar que los tokens terminen en los logs.

Pruebas con app.inject()

No es necesario abrir un puerto para probar una aplicación Fastify. app.inject() ejecuta una solicitud a través de todo el stack, incluyendo el routing, la validación y los hooks, y devuelve un objeto de respuesta sobre el cual puedes realizar aserciones.

const app = buildApp();
await app.ready();

const res = await app.inject({
  method: "POST",
  url: "/users",
  payload: { name: "Ada", email: "[email protected]" },
});

assert.equal(res.statusCode, 201);
assert.equal(res.json().name, "Ada");
await app.close();

Dos hábitos hacen que este proceso sea agradable. Primero, exporta una factory buildApp() desde app.ts y llama a listen() solo en server.ts, para que las pruebas nunca vinculen un puerto. Segundo, llama a await app.ready() antes de realizar la inyección, lo que obliga a los plugins y esquemas a terminar de cargar. Las pruebas se ejecutan rápido porque no hay un socket, y están aisladas porque cada prueba construye su propia instancia.

Mejores prácticas

  • Define esquemas de solicitud y respuesta para cada ruta y, posteriormente, deriva los tipos mediante un type provider.
  • Mantén la app factory y el listener en archivos separados para que las pruebas se ejecuten in-process.
  • Registra la infraestructura con fastify-plugin y el código de funcionalidades sin ello, para que los scopes mantengan su sentido.
  • Utiliza onRequest para la autenticación y preHandler para la autorización que requiera el cuerpo parseado.
  • Centraliza el formato de errores con setErrorHandler en lugar de añadir condicionales en cada handler.
  • Registra campos estructurados en los logs, oculta los secretos y nunca registres los cuerpos completos de las solicitudes.
  • Valida la configuración con @fastify/env y aplica un fail fast durante el arranque.
  • Llama a app.close() en las pruebas y en SIGTERM para que las conexiones se cierren correctamente.

Errores comunes

  • Registrar un plugin sin fastify-plugin y preguntarse por qué los decoradores están indefinidos en otras partes.
  • Olvidar await app.ready() en los tests, provocando que los schemas y plugins aún no se hayan cargado.
  • Usar JSON.stringify manualmente cuando un response schema serializaría los datos de forma más rápida y segura.
  • Omitir los response schemas, lo que permite que cualquier propiedad se filtre hacia los clientes.
  • Añadir hooks globales cuando un hook de plugin con scope evitaría afectar a rutas no relacionadas de forma inesperada.
  • Tratar los hooks como middleware y esperar que preHandler se ejecute antes del body parsing.
  • Ignorar la estructura del manejador de errores por defecto y romper los clientes de la API con respuestas inconsistentes.

Próximos pasos

Fastify es el siguiente paso natural después de Express cuando el rendimiento y la validación empiezan a ser prioritarios. Si tu equipo busca una arquitectura más estructurada con inyección de dependencias sobre Fastify, lee la guía de NestJS. Para un estilo similar basado en estándares web en runtimes de edge, consulta Hono. Y si el ciclo de vida de los plugins todavía te parece abstracto, repasa los conceptos básicos de Node.js que lo sustentan.

En la practica

Rutas, plugins y hooks

Las cuatro piezas de un servicio Fastify: una ruta tipada, una dependencia encapsulada, un hook de ciclo de vida y una prueba de inyección (inject test).

routes/users.ts
import type { FastifyPluginAsync } from "fastify";
import { Type, type Static } from "@sinclair/typebox";

const User = Type.Object({
  id: Type.String({ format: "uuid" }),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: "email" }),
});

const Params = Type.Object({
  id: Type.String({ format: "uuid" }),
});

export const userRoutes: FastifyPluginAsync = async (app) => {
  app.get<{ Params: Static<typeof Params> }>(
    "/users/:id",
    {
      schema: { params: Params, response: { 200: User } },
    },
    async (request, reply) => {
      const user = await app.users.findById(request.params.id);
      if (!user) return reply.callNotFound();
      return user;
    },
  );
};

Declarando el contrato

Un esquema valida y serializa en una sola declaración. Las comprobaciones manuales dispersan las mismas reglas en cada handler y tienden a desincronizarse con el tiempo.

Preferir
app.post("/users", {
  schema: {
    body: Type.Object({
      name: Type.String({ minLength: 1 }),
      email: Type.String({ format: "email" }),
    }),
  },
}, createUser);
Evitar
app.post("/users", async (request, reply) => {
  const { name, email } = request.body as any;
  if (typeof name !== "string") {
    return reply.code(400).send({ error: "invalid" });
  }
  // validation grows with every field
});

Compartiendo una dependencia

Usa decoradores dentro de un plugin para mantener la superficie de la instancia explícita. Adjuntar propiedades a la instancia manualmente es invisible para el ciclo de vida de Fastify.

Preferir
export default fp(async (app) => {
  app.decorate("users", createUserRepository());
});
Evitar
// Mutating the instance directly skips decorators,
// typing and the onClose lifecycle.
(app as any).users = createUserRepository();

Compromisos

¿Vale la pena la ceremonia de los esquemas en Fastify?

Fastify intercambia un poco más de declaración por velocidad, seguridad y estructura. El coste solo es perceptible en los proyectos más pequeños.

Strengths

  • Rápido con fundamento

    El router y los serializadores compilados son genuinamente más rápidos, por lo que el mismo hardware soporta más tráfico sin necesidad de reescribir el código.

  • Validación y docs desde una sola fuente

    El esquema que usas para validar también puede generar un documento OpenAPI y tipos de TypeScript, manteniendo los contratos sincronizados.

  • Estructura escalable

    La encapsulación de plugins otorga a cada funcionalidad su propio scope. Las dependencias se registran una vez y se reutilizan deliberadamente.

Trade-offs

  • Los esquemas requieren tiempo de aprendizaje

    JSON Schema es más verboso que un objeto de Zod, y los mensajes de error requieren personalización para ser amigables con los consumidores de la API.

  • El ecosistema es más pequeño

    Hay plugins para casi todo, pero muchas menos respuestas en Stack Overflow que para Express, por lo que leerás la documentación más a menudo.

  • La rigurosidad puede sorprender

    Los esquemas de respuesta eliminan las propiedades desconocidas por defecto. Esto es una funcionalidad, pero parece pérdida de datos hasta que entiendes la serialización.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender Fastify?

Nuestro tutorial interactivo te guia a traves de Fastify paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.