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:
onRequest— o ponto mais precoce, ideal para autenticação e IDs de requisição.preParsing— antes do corpo ser lido, para compressão ou verificações de tamanho.preValidation— após o parsing, antes da validação de schema.preHandler— após a validação, logo antes do handler.preSerializationeonSend— moldam o payload na saída.onResponseeonError— 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-plugine o código de feature sem ele, para que os escopos permaneçam significativos. - Use
onRequestpara autenticação epreHandlerpara autorização que precise do body parseado. - Centralize a formatação de erros com
setErrorHandlerem 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/enve utilize fail fast na inicialização. - Chame
app.close()nos testes e noSIGTERMpara que as conexões sejam encerradas corretamente.
Erros comuns
- Registrar um plugin sem
fastify-plugine 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.stringifymanualmente 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
preHandlerseja 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.