12k
All articles

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.

OpenReplay Team
OpenReplay Team
Como Construir uma REST API com Fastify

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 response no 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, body e response deve ser um JSON Schema completo que inclua uma propriedade type.
  • Compartilhe um cliente de banco de dados entre rotas envolvendo o plugin em fastify-plugin para quebrar deliberadamente o encapsulamento e, em seguida, anexe o cliente com fastify.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çãoExpress (típico)Fastify (idiomático)
Validação de entradaManual ou middleware (express-validator)JSON Schema declarado na rota
Serialização de respostaJSON.stringifyfast-json-stringify compilado a partir de um response schema
Compartilhamento de recursosMiddleware global em um req compartilhadoPlugins encapsulados + decorators
Ciclo de vida da requisiçãoCadeia linear de middlewaresHooks 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

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:

  • 201 com o recurso criado no POST;
  • 200 nas leituras;
  • 204 sem corpo no DELETE; e
  • 404 quando 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: onRequestpreParsingpreValidationpreHandler → handler → preSerializationonSendonResponse, 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:

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.

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.