Node.js Framework

Fastify

Fastify é um framework web para Node.js rápido e focado em schemas. O JSON Schema compila sua validação e serialização, enquanto plugins encapsulados mantêm a integridade de serviços de grande porte.

intermediate15 min readUpdated 16 de set. de 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" });
Lançado
2016
Roda em
Node.js
Estilo
Schema-first, baseado em plugins
Ideia central
JSON Schema
Linguagem
JavaScript / TypeScript
Versão
5.x

Por que importa

Por que o Fastify vence em throughput

Throughput por design

Um roteador radix-tree e schemas compilados permitem que o Fastify processe muito mais requisições por segundo do que o Express, mantendo a API familiar.

Validação que compila

Um JSON Schema não é apenas documentação. O Fastify o compila em um validador e um serializador, portanto, entradas inválidas são rejeitadas antes mesmo do seu handler ser executado.

Plugins encapsulados

Cada plugin possui seu próprio escopo. Decorators e hooks permanecem locais, a menos que você os compartilhe explicitamente, evitando que serviços grandes sofram vazamento de estado.

O panorama completo

As três ideias por trás do Fastify

Declare a forma dos seus dados, envolva funcionalidades em plugins encapsulados e intercepte a requisição em pontos bem definidos do ciclo de vida.

JSON Schema

Declarar

Descreva params, body, query e respostas uma única vez, e o Fastify fará a validação e serialização para você.

Plugins

Encapsular

Um plugin é uma função que pode decorar a instância e registrar rotas dentro de seu próprio escopo.

Hooks

Interceptar

Hooks de ciclo de vida como onRequest, preHandler e onSend rodam em pontos precisos de cada requisição.

O guia completo

Fastify: Tudo que voce precisa saber

O que é Fastify?

Fastify é um web framework para Node.js construído com base em uma premissa: se você descrever seus dados, o framework pode fazer a maior parte do trabalho. Ele combina um roteador extremamente rápido com um sistema de schemas, um modelo de plugins com escopo e um ciclo de vida de hooks. O resultado é um framework que é rápido e rigoroso quanto à segurança, sem forçar uma estrutura de pastas específica.

Surgiu em 2016 e alcançou a versão estável 1.0 em 2018. Hoje, ele alimenta APIs em produção em empresas que precisam de uma ergonomia semelhante à do Express, mas com uma performance visivelmente superior e uma estratégia real de validação. Se você conhece Express, a maior parte do Fastify parecerá familiar em poucas horas.

Schema-first: validação que compila

A característica definidora é que os schemas são executáveis. Quando você anexa um JSON Schema a uma rota, o Fastify o compila uma única vez na inicialização e utiliza o validador compilado para cada requisição. Não ocorre interpretação a cada chamada, e é por isso que a validação tem um custo quase zero.

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);
});

Existem dois benefícios principais. Primeiro, requisições inválidas são rejeitadas com um 400 antes que seu handler seja chamado, portanto, a lógica de negócio sempre recebe apenas dados limpos. Segundo, o mesmo schema impulsiona a serialização: o Fastify compila o schema de resposta em uma função fast-json-stringify, que é drasticamente mais rápida do que JSON.stringify e garante que você nunca exponha campos que não declarou.

Rotas e opções de rota

Uma rota é composta por um método, um caminho e um handler, mas a parte interessante é o objeto de opções entre eles. É onde residem o schema, o handler e os metadados por rota.

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

Os métodos abreviados (app.get, app.post e assim por diante) aceitam as mesmas opções como seu segundo argumento. Os parâmetros da rota chegam em request.params, a query string em request.query e o corpo parseado em request.body. Como as formas vêm do schema, o TypeScript reconhece os três.

reply é a outra metade do handler. reply.code(404), reply.header(...) e reply.send(...) espelham o Express, e retornar um valor de um handler async é um atalho para reply.send. O Fastify também fornece helpers como reply.callNotFound() para que os caminhos de erro permaneçam consistentes.

Plugins e encapsulamento

O Fastify não possui uma cadeia de middleware no sentido do Express. Em vez disso, tudo é um plugin, e cada plugin possui seu próprio escopo. Essa regra única é o que mantém aplicações grandes previsíveis.

import fp from "fastify-plugin";

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

await app.register(dbPlugin);

Sem o fastify-plugin, os decorators e hooks adicionados dentro do plugin seriam visíveis apenas para as rotas registradas dentro desse mesmo plugin. Isso é encapsulamento: uma funcionalidade pode ter suas próprias dependências e configurações sem poluir o restante do app. Envolver com fp remove deliberadamente essa fronteira quando você está construindo infraestruturas compartilhadas, como um banco de dados ou um logger.

Decorators são a maneira idiomática de anexar funcionalidades: app.decorate("users", repo) para a instância, app.decorateRequest("user", null) para a request e app.decorateReply para a reply. Como eles são tipados via module augmentation, você tem autocomplete em vez de any.

O ciclo de vida: hooks em ordem

Hooks permitem que você execute código em pontos definidos sem a necessidade de envolver os handlers. Eles são executados em uma ordem fixa:

  1. onRequest — o ponto mais precoce, ideal para autenticação e IDs de requisição.
  2. preParsing — antes do corpo ser lido, para compressão ou verificações de tamanho.
  3. preValidation — após o parsing, antes da validação de schema.
  4. preHandler — após a validação, logo antes do handler.
  5. preSerialization e onSend — moldam o payload na saída.
  6. onResponse e onError — observam a requisição 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");
});

Hooks possuem escopo semelhante aos plugins, portanto, um hook adicionado dentro de um plugin será executado apenas para as rotas daquele plugin. onClose é a contraparte para o shutdown, e é onde você libera pools de banco de dados e timers. Acertar a ordem é a principal habilidade; a documentação a lista com precisão e ela raramente muda.

TypeScript sem burocracia

O Fastify é escrito em TypeScript e seus tipos são tratados como cidadãos de primeira classe. Você tipa uma rota passando parâmetros genéricos, e os erros de validação aparecem no momento da compilação, em vez de em produção.

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

O padrão mais valioso são os type providers. Com @fastify/type-provider-typebox, o próprio schema torna-se o tipo, portanto, você nunca escreve a interface duas vezes e evita que as duas fiquem dessincronizadas.

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
});

O module augmentation é a forma como os decorators obtêm seus tipos: declare a propriedade em FastifyInstance ou FastifyRequest, e ela estará disponível em qualquer lugar. Quando algo é unknown, isso geralmente é um sinal de que um schema ou uma declaração está faltando.

Logging com pino, nativo

O Fastify já vem com o pino como seu logger. Ative-o com apenas uma opção e cada requisição terá uma linha de log em JSON estruturado com um id, timing e seus próprios 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);
});

Por ser pino, a saída é um JSON delimitado por novas linhas que é enviado facilmente para qualquer agregador de logs, e request.log inclui automaticamente o contexto da requisição. Em desenvolvimento, utilize o pipe para o pino-pretty para ter uma saída legível; em produção, mantenha o JSON. Regras de redação (redaction) são a maneira mais segura de evitar que tokens apareçam nos logs.

Testando com app.inject()

Você não precisa abrir uma porta para testar um app Fastify. app.inject() executa uma requisição através de todo o stack, incluindo roteamento, validação e hooks, e retorna um objeto de resposta para que você possa fazer as asserções.

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();

Dois hábitos tornam isso agradável. Primeiro, exporte uma factory buildApp() de app.ts e chame listen() apenas em server.ts, para que os testes nunca ocupem uma porta. Segundo, chame await app.ready() antes de injetar, o que força os plugins e schemas a terminarem de carregar. Os testes rodam rápido porque não há socket, e são isolados porque cada teste constrói sua própria instância.

Melhores práticas

  • Defina esquemas de request e response para cada rota e, em seguida, derive os tipos com um type provider.
  • Mantenha a app factory e o listener em arquivos separados para que os testes permaneçam in-process.
  • Registre a infraestrutura com fastify-plugin e o código de feature sem ele, para que os escopos permaneçam significativos.
  • Use onRequest para autenticação e preHandler para autorização que precise do body parseado.
  • Centralize a formatação de erros com setErrorHandler em vez de criar ramificações em cada handler.
  • Registre logs com campos estruturados, oculte segredos e nunca registre corpos de request completos.
  • Valide a configuração com @fastify/env e utilize fail fast na inicialização.
  • Chame app.close() nos testes e no SIGTERM para que as conexões sejam encerradas corretamente.

Erros comuns

  • Registrar um plugin sem fastify-plugin e questionar por que os decorators estão indefinidos em outros lugares.
  • Esquecer o await app.ready() nos testes, fazendo com que schemas e plugins ainda não tenham sido carregados.
  • Usar JSON.stringify manualmente quando um response schema faria a serialização de forma mais rápida e segura.
  • Omitir response schemas, o que significa que qualquer propriedade pode vazar para os clientes.
  • Adicionar hooks globais quando um hook de plugin com escopo evitaria afetar rotas não relacionadas.
  • Tratar hooks como middleware e esperar que preHandler seja executado antes do body parsing.
  • Ignorar a estrutura do manipulador de erros padrão e quebrar clientes da API com respostas inconsistentes.

Próximos passos

O Fastify é a evolução natural do Express quando a vazão (throughput) e a validação passam a ser prioridades. Se a sua equipe busca uma arquitetura opinativa com injeção de dependência sobre o Fastify, leia o guia do NestJS. Para seguir o mesmo estilo de web-standards em edge runtimes, veja o Hono. E se o ciclo de vida dos plugins ainda parecer abstrato, revise os fundamentos de Node.js que sustentam tudo isso.

Na pratica

Rotas, plugins e hooks

As quatro peças de um serviço Fastify: uma rota tipada, uma dependência encapsulada, um hook de ciclo de vida e um teste de inject.

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 o contrato

Um schema valida e serializa em uma única declaração. Verificações manuais espalham as mesmas regras por todos os handlers e tendem a divergir com o tempo.

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
});

Compartilhando uma dependência

Use decorate dentro de um plugin para manter a superfície da instância explícita. Anexar propriedades à instância manualmente é invisível para o ciclo de vida do 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();

Trade-offs

O Fastify vale a 'cerimônia' dos schemas?

O Fastify troca um pouco mais de declaração por velocidade, segurança e estrutura. Esse custo só é perceptível em projetos muito pequenos.

Strengths

  • Rápido com propósito

    O roteador e os serializadores compilados são genuinamente mais rápidos, permitindo que o mesmo hardware suporte mais tráfego sem a necessidade de reescrita.

  • Validação e docs de uma única fonte

    O schema usado para validação também pode gerar um documento OpenAPI e tipos TypeScript, mantendo os contratos sincronizados.

  • Estrutura que escala

    O encapsulamento de plugins dá a cada funcionalidade seu próprio escopo. Dependências são registradas uma vez e reutilizadas deliberadamente.

Trade-offs

  • Schemas levam tempo para aprender

    JSON Schema é mais verboso que um objeto Zod, e as mensagens de erro precisam de customização para serem amigáveis aos consumidores da API.

  • O ecossistema é menor

    Existe um plugin para quase tudo, mas há muito menos respostas no Stack Overflow do que para o Express, então você lerá a documentação com mais frequência.

  • A rigidez pode surpreender

    Schemas de resposta removem propriedades desconhecidas por padrão. Isso é uma funcionalidade, mas parece perda de dados até que você entenda a serialização.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender Fastify?

Nosso tutorial interativo te guia por Fastify passo a passo — com quizzes e codigo real que voce pode executar no navegador.