¿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:
onRequest— el punto más temprano, ideal para autenticación e IDs de solicitud.preParsing— antes de que se lea el cuerpo, para compresión o comprobaciones de tamaño.preValidation— después del parsing, antes de la validación del esquema.preHandler— después de la validación, justo antes del handler.preSerializationyonSend— dan forma al payload a la salida.onResponseyonError— 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-pluginy el código de funcionalidades sin ello, para que los scopes mantengan su sentido. - Utiliza
onRequestpara la autenticación ypreHandlerpara la autorización que requiera el cuerpo parseado. - Centraliza el formato de errores con
setErrorHandleren 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/envy aplica un fail fast durante el arranque. - Llama a
app.close()en las pruebas y enSIGTERMpara que las conexiones se cierren correctamente.
Errores comunes
- Registrar un plugin sin
fastify-pluginy 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.stringifymanualmente 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
preHandlerse 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.