Como Construir uma REST API com Fastify
Crie uma API REST com Fastify v5 e Prisma, com validação JSON Schema, plugins, decoradores, hooks e tipagem TypeBox para CRUD de posts.
Fastify é um framework web para Node.js de alta performance e baixo overhead, cuja característica definidora é a validação e serialização orientadas por JSON Schema: você declara a estrutura de cada requisição e resposta, e o Fastify compila esses schemas em funções otimizadas na inicialização. Até onde os mantenedores sabem, o Fastify é um dos frameworks web mais rápidos disponíveis, dependendo da complexidade do código, servindo mais de 76 mil requisições por segundo. Este tutorial constrói uma REST API CRUD completa para um recurso posts no Fastify v5 com Prisma como camada de dados, e foca nos quatro primitivos que diferenciam o Fastify do Express — plugins encapsulados, decorators, validação/serialização baseada em schema e lifecycle hooks.
Principais Conclusões
- O Fastify v5 requer Node.js v20 ou superior; a v4 chegou ao fim de vida em 30 de junho de 2025, e a v3 e anteriores não são mais mantidas, portanto novas APIs devem começar na linha v5.x.
- Um schema de
responseno Fastify tem dupla função — torna a serialização mais rápida ao compilar um stringifier dedicado, e elimina qualquer propriedade não declarada no schema, evitando que campos internos vazem nas respostas. - A partir da v5, o schema shorthand foi removido: cada schema de
querystring,params,bodyeresponsedeve ser um JSON Schema completo que inclua uma propriedadetype. - Compartilhe um cliente de banco de dados entre rotas envolvendo o plugin em
fastify-pluginpara quebrar deliberadamente o encapsulamento e, em seguida, anexe o cliente comfastify.decorate('prisma', client). - Com o type provider TypeBox, você declara cada schema uma única vez e o Fastify infere os tipos de requisição e resposta a partir dele, eliminando divergências entre as regras de validação e os tipos TypeScript.
Por que Fastify: os quatro primitivos que o Express não possui
Você pode escrever os mesmos endpoints CRUD no Express. O que você não consegue reproduzir facilmente é o modelo do Fastify, construído sobre quatro primitivos: plugins encapsulados, decorators, validação e serialização baseadas em schema, e lifecycle hooks. Os mantenedores destacam que o Fastify é totalmente extensível por meio de seus hooks, plugins e decorators. Estes não são recursos adicionados a um roteador; eles são a arquitetura em si.
| Preocupação | Express (típico) | Fastify (idiomático) |
|---|---|---|
| Validação de entrada | Manual ou middleware (express-validator) | JSON Schema declarado na rota |
| Serialização de resposta | JSON.stringify | fast-json-stringify compilado a partir de um response schema |
| Compartilhamento de recursos | Middleware global em um req compartilhado | Plugins encapsulados + decorators |
| Ciclo de vida da requisição | Cadeia linear de middlewares | Hooks tipados (onRequest, preHandler, onError, …) |
A afirmação de performance é mecânica, não marketing. Embora não seja obrigatório, o Fastify recomenda o uso de JSON Schema para validar rotas e serializar saídas; internamente, o Fastify compila o schema em uma função de alto desempenho. Um response schema é transformado em um serializador especializado na inicialização, de modo que a produção de JSON ignora o caminho genérico do JSON.stringify. Trate os números oficiais pelo que são: esses benchmarks são obtidos usando um benchmark sintético de “hello world” que visa avaliar o overhead do framework — faça benchmark da sua própria aplicação antes de citar qualquer número.
Configuração do projeto e um servidor v5 mínimo
Discover how at OpenReplay.com.
Comece com ESM, pois tanto o Fastify quanto o Prisma v7 são ESM-first. O Fastify v5 suportará apenas Node.js v20+; se você estiver usando uma versão mais antiga do Node.js, precisará fazer upgrade para uma versão mais recente para usar o Fastify v5.
mkdir fastify-posts-api && cd fastify-posts-api
npm init -y
npm pkg set type="module"
npm i fastify@5
A versão estável atual é a 5.9.0, publicada em 28 de junho de 2026. Crie o arquivo server.js:
import Fastify from 'fastify'
const app = Fastify({ logger: true })
app.get('/health', async () => ({ status: 'ok' }))
try {
await app.listen({ port: 3000 })
} catch (err) {
app.log.error(err)
process.exit(1)
}
Observe a chamada ao listen no formato de objeto. A assinatura de argumentos variádicos do método .listen() foi removida na v5, portanto você não pode mais chamar .listen() com um número variável de argumentos — sempre passe um objeto de opções como { port: 3000 }. Execute com node --watch server.js e acesse http://localhost:3000/health.
Roteamento à maneira do Fastify: plugins e encapsulamento
No Fastify, as rotas residem dentro de plugins, e cada plugin é executado em seu próprio contexto de encapsulamento. Um decorator, hook ou rota registrado dentro de um plugin é visível para aquele plugin e seus filhos, mas invisível para seus irmãos — o oposto do middleware do Express, onde tudo compartilha um único objeto de requisição global. A referência de Plugins documenta esse modelo de escopo.
Um plugin é apenas uma função assíncrona que recebe a instância encapsulada:
// routes/posts.js
export default async function postsRoutes(app, opts) {
app.get('/', async (request, reply) => {
return [] // o handler de listagem vem depois
})
}
Registre-o com um prefixo em server.js:
import postsRoutes from './routes/posts.js'
app.register(postsRoutes, { prefix: '/api/posts' })
Mantenha as funções de plugin consistentemente assíncronas. O guia de migração v5 é explícito ao afirmar que todas as depreciações da v4 foram removidas e não funcionarão mais após o upgrade, e misturar os estilos de callback e promise dentro de um único plugin — tolerado na v4 — não é mais permitido. Escolha async/await e mantenha esse padrão.
Validação e serialização com schema
Anexe um objeto schema a qualquer rota e o Fastify validará o body, params, querystring e headers recebidos, e então serializará o payload de saída contra o schema de response. Este é o recurso característico do framework, documentado em Validation and Serialization.
Uma regra da v5 se aplica a todos os schemas que você escrever: o shorthand foi removido. A partir da v5, você deve fornecer um JSON Schema completo para querystring, params, body e response, cada um incluindo a propriedade type, ou a rota não será validada — o guia de migração v5 registra a remoção da opção jsonShortHand.
Defina os schemas para o recurso posts:
// schemas/posts.js
export const postResponse = {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
content: { type: 'string' },
published: { type: 'boolean' },
createdAt: { type: 'string' }
}
}
export const createPostBody = {
type: 'object',
required: ['title', 'content'],
properties: {
title: { type: 'string', minLength: 1 },
content: { type: 'string', minLength: 1 },
published: { type: 'boolean', default: false }
}
}
export const idParams = {
type: 'object',
required: ['id'],
properties: { id: { type: 'string' } }
}
O schema de response é a parte que a maioria dos tutoriais ignora, e ele justifica sua presença de duas formas. Ele é compilado em um serializador dedicado que é mais rápido do que a stringificação genérica, e atua como uma lista de permissões: qualquer propriedade que seu handler retorne e que não esteja declarada no schema é descartada antes de chegar ao cliente. Um passwordHash ou userId interno que entre sorrateiramente em um resultado de query nunca sai do servidor, pois o serializador simplesmente não sabe como emiti-lo.
O caminho TypeScript com TypeBox
Se você está usando TypeScript, o problema é manter schemas de validação e definições de tipo sincronizados. O type provider TypeBox os consolida em uma única declaração. Instale typebox como peer dependency obrigatório e @fastify/type-provider-typebox:
npm i typebox @fastify/type-provider-typebox
Registre o provider e declare cada schema uma única vez com Type:
import Fastify from 'fastify'
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>()
app.post('/api/posts', {
schema: {
body: Type.Object({
title: Type.String({ minLength: 1 }),
content: Type.String({ minLength: 1 })
})
}
}, async (request) => {
// request.body é tipado como { title: string; content: string }
const { title, content } = request.body
return { title, content }
})
Você importa Type e TypeBoxTypeProvider de @fastify/type-provider-typebox e chama Fastify().withTypeProvider<TypeBoxTypeProvider>(); os tipos dos campos do body são então inferidos automaticamente. Use o pacote com escopo — o import legado de @sinclair/typebox mostrado em guias mais antigos é o nome anterior à v5. Uma sutileza da v5 a ter em mente: o guia de migração observa que os type providers foram divididos em dois tipos separados: ValidatorSchema e SerializerSchema, portanto atualize o pacote do provider junto com o Fastify. Note também que os tipos do provider não se propagam globalmente; em uso encapsulado, é possível remapear o contexto para usar um ou mais providers, o que significa que você redeclara o provider em cada plugin que precisar dele.
A camada de banco de dados como um decorator plugin
Conecte o banco de dados à instância do Fastify com um decorator, exposto por meio de um plugin que quebra deliberadamente o encapsulamento. Para compartilhar um recurso — um cliente de banco de dados — entre todas as rotas, envolva o plugin em fastify-plugin para que a decoração escape do seu próprio escopo, e então anexe-o com fastify.decorate('prisma', client). A referência de Decorators cobre esse padrão.
Este tutorial usa o Prisma (v7, atualmente 7.8.x), que é livre de Rust e exclusivamente ESM — uma combinação limpa com a configuração ESM acima. Instale-o, defina o schema e gere o client. O Prisma 7 também requer um driver adapter para criar o client, portanto instale o adapter SQLite junto com os pacotes principais:
npm i @prisma/client@7 @prisma/adapter-better-sqlite3
npm i -D prisma@7
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Post {
id String @id @default(uuid())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
}
O Prisma 7 configura sua CLI por meio de um arquivo prisma.config.ts e gera o client no caminho de output definido acima; execute npx prisma migrate dev para criar o banco de dados e o client. Em seguida, exponha-o como um decorator. Note que no Prisma 7 a chamada simples new PrismaClient() foi removida — você deve construir o client com um driver adapter:
// plugins/prisma.js
import fp from 'fastify-plugin'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { PrismaClient } from '../generated/prisma/client.js'
export default fp(async (app) => {
const adapter = new PrismaBetterSqlite3({ url: 'file:./dev.db' })
const prisma = new PrismaClient({ adapter })
await prisma.$connect()
app.decorate('prisma', prisma)
app.addHook('onClose', async (instance) => {
await instance.prisma.$disconnect()
})
})
Como o plugin está envolvido em fp, app.prisma agora está acessível em todas as rotas da aplicação.
Prefere Postgres com TypeORM? O padrão de decorator é idêntico: conecte o DataSource, depois app.decorate('db', dataSource). Apenas a camada de schema muda.
Handlers CRUD com códigos de status corretos
Implemente as cinco operações como rotas dentro do plugin de posts, cada uma com seu schema. Retorne o código de status correto para cada verbo:
201com o recurso criado no POST;200nas leituras;204sem corpo no DELETE; e404quando uma busca não encontrar o recurso.
O Fastify define o Content-Type e serializa contra seu response schema automaticamente — consulte a Reply API.
// routes/posts.js
import { postResponse, createPostBody, idParams } from '../schemas/posts.js'
export default async function postsRoutes(app) {
// CREATE
app.post('/', {
schema: { body: createPostBody, response: { 201: postResponse } }
}, async (request, reply) => {
const post = await app.prisma.post.create({ data: request.body })
return reply.code(201).send(post)
})
// READ all
app.get('/', {
schema: { response: { 200: { type: 'array', items: postResponse } } }
}, async () => {
return app.prisma.post.findMany({ orderBy: { createdAt: 'desc' } })
})
// READ one
app.get('/:id', {
schema: { params: idParams, response: { 200: postResponse } }
}, async (request, reply) => {
const post = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!post) return reply.code(404).send({ message: 'Post not found' })
return post
})
// UPDATE
app.put('/:id', {
schema: { params: idParams, body: createPostBody, response: { 200: postResponse } }
}, async (request, reply) => {
const exists = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!exists) return reply.code(404).send({ message: 'Post not found' })
return app.prisma.post.update({ where: { id: request.params.id }, data: request.body })
})
// DELETE
app.delete('/:id', {
schema: { params: idParams }
}, async (request, reply) => {
const exists = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!exists) return reply.code(404).send({ message: 'Post not found' })
await app.prisma.post.delete({ where: { id: request.params.id } })
return reply.code(204).send()
})
}
Um detalhe da v5 que vale destacar no DELETE: na v4, o Fastify permitia requisições DELETE com um header Content-Type: application/json e corpo vazio; isso não é mais permitido na v5. Não envie o header Content-Type quando não houver payload.
Tratamento de erros, hooks e hardening para produção
Centralize o tratamento de erros com setErrorHandler e intercepte o ciclo de vida da requisição com hooks — os dois pontos de extensão que substituem a cadeia de middlewares do Express. A referência de Hooks lista o fluxo de requisição em ordem: onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse, com onError sendo disparado quando algo lança uma exceção.
class NotFoundError extends Error {
constructor (message) { super(message); this.statusCode = 404 }
}
app.setErrorHandler((error, request, reply) => {
request.log.error(error)
const status = error.statusCode ?? 500
reply.code(status).send({
error: error.name,
message: status === 500 ? 'Internal Server Error' : error.message
})
})
// exemplo de hook preHandler: bloqueia uma rota antes do handler ser executado
app.addHook('preHandler', async (request) => {
request.log.info({ url: request.url }, 'incoming request')
})
Uma falha de validação é interrompida antes que seu handler seja executado, retornando um 400 automaticamente — uma das razões pelas quais schemas reduzem o código dos handlers. Para medir tempo dentro de um hook, use reply.elapsedTime; o método reply.getResponseTime() foi removido na v5, e você deve usar reply.elapsedTime em seu lugar.
A partir daqui, o checklist de produção consiste na instalação de plugins, cada um um pacote @fastify/* com escopo que se registra da mesma forma que seu plugin de rotas:
@fastify/jwtpara autenticação;@fastify/swaggerpara gerar documentação OpenAPI diretamente dos seus schemas de rota;@fastify/rate-limitpara throttling; e@fastify/corspara acesso cross-origin.
Para fluxos de autenticação mais avançados e implantação, consulte guias dedicados — essas são construções separadas. Se você quiser ver como esses endpoints se comportam sob tráfego real, combine a API com monitoramento de erros Node.js no frontend que a consome.
Você agora tem uma API CRUD Fastify v5 funcional onde schemas validam a entrada, serializam a saída e se autodocumentam, e onde o banco de dados é compartilhado por meio de um decorator em vez de uma importação global. O próximo passo concreto: adicione @fastify/swagger, aponte-o para os schemas que você já escreveu e veja a especificação OpenAPI se gerar automaticamente — prova de que no Fastify o schema é a fonte da verdade, não uma reflexão tardia.
Perguntas Frequentes
Qual é a diferença entre fastify.register e fastify.decorate?
O método register monta um plugin em seu próprio contexto de encapsulamento, de modo que rotas, hooks e decorators adicionados dentro dele permanecem com escopo naquele plugin e seus filhos. O método decorate anexa uma propriedade ou método reutilizável diretamente na instância do Fastify, na requisição ou na resposta. Normalmente você usa decorate para expor recursos compartilhados como um cliente de banco de dados, e envolve esse plugin em fastify-plugin para que a decoração escape do encapsulamento e fique disponível em toda a aplicação.
O Fastify é mais rápido que o Express, e por quanto?
O próprio benchmark do Fastify reporta servir até 76 mil ou mais requisições por segundo, mas os mantenedores ressaltam que este é um teste sintético de 'hello world' que mede o overhead do framework, não o throughput em cenários reais. A vantagem mecânica é concreta: o Fastify compila JSON Schemas em funções especializadas de validação e serialização na inicialização, de modo que produzir uma resposta ignora o overhead genérico do JSON.stringify. Sempre faça benchmark da sua própria aplicação antes de citar qualquer multiplicador de velocidade.
Por que o schema da minha rota Fastify v5 falha na validação quando funcionava na v4?
A partir da v5, o schema shorthand e a opção jsonShortHand foram removidos, portanto cada schema de querystring, params, body e response deve ser um JSON Schema completo que inclua explicitamente uma propriedade type. Um schema que apenas declara propriedades sem um type no nível raiz não será mais validado. Adicione type 'object' a cada objeto de schema. O guia de migração v5 documenta isso como uma mudança breaking intencional em relação ao comportamento shorthand da v4.
Posso usar o Fastify com TypeScript sem escrever os tipos duas vezes?
Sim. Instale typebox como peer dependency mais o pacote com escopo @fastify/type-provider-typebox, depois chame Fastify().withTypeProvider com o generic TypeBoxTypeProvider. Você declara cada schema uma única vez usando Type, e o Fastify infere os tipos de requisição e resposta a partir dele, eliminando divergências entre as regras de validação e as interfaces TypeScript. Importe Type de @fastify/type-provider-typebox, não do nome legado @sinclair/typebox mostrado em guias mais antigos, e redeclare o provider em cada plugin que precisar dele.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k