TypeScript

Dominando APIs REST com Fastify e TypeScript: Schemas, Plugins e Types em Projetos Reais

14 min de leitura

Dominando APIs REST com Fastify e TypeScript: Schemas, Plugins e Types em Projetos Reais

Fundamentos de APIs REST com Fastify Fastify é um framework web moderno construído sobre Node.js, conhecido por sua velocidade e baixa sobrecarga de memória. Ao contrário de frameworks mais antigos, Fastify foi projetado desde o início para ser altamente performático, utilizando técnicas como compilação Just-In-Time (JIT) de rotas e serialização eficiente de JSON. Quando combinado com TypeScript, ganhamos segurança de tipos que previne erros em tempo de compilação e melhora significativamente a manutenibilidade do código. Uma API REST no Fastify segue os princípios RESTful: utiliza métodos HTTP apropriados (GET, POST, PUT, DELETE), retorna dados em formato padrão (geralmente JSON) e mantém endpoints sem estado. O framework abstrai a complexidade do protocolo HTTP, permitindo que você se concentre na lógica de negócio. TypeScript adiciona uma camada extra de segurança ao forçar tipagem estática, evitando bugs silenciosos que apenas apareceríam em produção. Instalação e Configuração Inicial Para começar, você precisa criar um projeto Node.js com TypeScript. Primeiro, inicialize o projeto: Crie um

<h2>Fundamentos de APIs REST com Fastify</h2>

<p>Fastify é um framework web moderno construído sobre Node.js, conhecido por sua velocidade e baixa sobrecarga de memória. Ao contrário de frameworks mais antigos, Fastify foi projetado desde o início para ser altamente performático, utilizando técnicas como compilação Just-In-Time (JIT) de rotas e serialização eficiente de JSON. Quando combinado com TypeScript, ganhamos segurança de tipos que previne erros em tempo de compilação e melhora significativamente a manutenibilidade do código.</p>

<p>Uma API REST no Fastify segue os princípios RESTful: utiliza métodos HTTP apropriados (GET, POST, PUT, DELETE), retorna dados em formato padrão (geralmente JSON) e mantém endpoints sem estado. O framework abstrai a complexidade do protocolo HTTP, permitindo que você se concentre na lógica de negócio. TypeScript adiciona uma camada extra de segurança ao forçar tipagem estática, evitando bugs silenciosos que apenas apareceríam em produção.</p>

<h3>Instalação e Configuração Inicial</h3>

<p>Para começar, você precisa criar um projeto Node.js com TypeScript. Primeiro, inicialize o projeto:</p>

<pre><code class="language-bash">npm init -y

npm install fastify

npm install -D typescript @types/node ts-node tsx

npx tsc --init</code></pre>

<p>Crie um arquivo <code>src/server.ts</code> com uma aplicação básica:</p>

<pre><code class="language-typescript">import Fastify from &#039;fastify&#039;;

const fastify = Fastify({ logger: true });

fastify.get(&#039;/&#039;, async (request, reply) =&gt; {

return { message: &#039;Olá, Fastify!&#039; };

});

const start = async () =&gt; {

try {

await fastify.listen({ port: 3000 });

console.log(&#039;Servidor rodando em http://localhost:3000&#039;);

} catch (err) {

fastify.log.error(err);

process.exit(1);

}

};

start();</code></pre>

<p>Execute com <code>npx tsx src/server.ts</code>. O arquivo <code>tsconfig.json</code> deve ter <code>&quot;strict&quot;: true</code> habilitado para garantir tipagem rigorosa. Esta é a base sobre a qual construiremos recursos mais avançados.</p>

<h2>Schemas e Validação de Dados</h2>

<p>Schemas em Fastify são definições estruturadas que descrevem como seus dados devem ser formados. Eles servem para duas finalidades críticas: validação de entrada (body, query parameters, headers) e serialização de saída (response). Fastify utiliza JSON Schema por padrão, que é um padrão de indústria amplamente adotado. A validação acontece automaticamente antes do seu handler ser executado, economizando linhas de código e prevenindo dados malformados.</p>

<h3>Definindo Schemas com JSON Schema</h3>

<p>Um schema JSON descreve a estrutura esperada dos dados. Considere um endpoint que cria um usuário:</p>

<pre><code class="language-typescript">import Fastify, { FastifyInstance } from &#039;fastify&#039;;

const fastify = Fastify({ logger: true });

// Define o schema de entrada (body)

const createUserSchema = {

body: {

type: &#039;object&#039;,

required: [&#039;name&#039;, &#039;email&#039;],

properties: {

name: { type: &#039;string&#039;, minLength: 3 },

email: { type: &#039;string&#039;, format: &#039;email&#039; },

age: { type: &#039;integer&#039;, minimum: 0 },

},

},

// Define o schema de saída (response)

response: {

200: {

type: &#039;object&#039;,

properties: {

id: { type: &#039;string&#039; },

name: { type: &#039;string&#039; },

email: { type: &#039;string&#039; },

createdAt: { type: &#039;string&#039; },

},

},

},

};

interface User {

id: string;

name: string;

email: string;

age?: number;

createdAt: string;

}

fastify.post&lt;{ Body: { name: string; email: string; age?: number } }&gt;(

&#039;/users&#039;,

{ schema: createUserSchema },

async (request, reply) =&gt; {

// O body já foi validado automaticamente

const { name, email, age } = request.body;

const newUser: User = {

id: Math.random().toString(36).substring(7),

name,

email,

age,

createdAt: new Date().toISOString(),

};

return newUser;

}

);

fastify.listen({ port: 3000 });</code></pre>

<p>Quando alguém envia um POST sem o campo <code>name</code>, ou com um <code>email</code> inválido, Fastify automaticamente retorna um erro 400 com detalhes do problema. Não é necessário validar manualmente. O schema também documenta sua API implicitamente.</p>

<h3>Type Safety com Interfaces</h3>

<p>TypeScript permite que você defina interfaces que correspondem aos seus schemas, garantindo consistência entre validação e tipagem:</p>

<pre><code class="language-typescript">// Defina seus tipos primeiro

interface CreateUserRequest {

name: string;

email: string;

age?: number;

}

interface UserResponse {

id: string;

name: string;

email: string;

age?: number;

createdAt: string;

}

// Reutilize em múltiplos lugares

const userSchema = {

body: {

type: &#039;object&#039;,

required: [&#039;name&#039;, &#039;email&#039;],

properties: {

name: { type: &#039;string&#039;, minLength: 3 },

email: { type: &#039;string&#039;, format: &#039;email&#039; },

age: { type: &#039;integer&#039;, minimum: 0 },

},

} as const,

};

fastify.post&lt;{ Body: CreateUserRequest; Reply: UserResponse }&gt;(

&#039;/users&#039;,

{ schema: userSchema },

async (request, reply) =&gt; {

// request.body é tipado como CreateUserRequest

// reply é tipado para retornar UserResponse

return {

id: &#039;123&#039;,

name: request.body.name,

email: request.body.email,

age: request.body.age,

createdAt: new Date().toISOString(),

};

}

);</code></pre>

<p>Esta abordagem elimina inconsistências: seu schema JSON Schema e sua interface TypeScript são a fonte única da verdade sobre a estrutura de dados.</p>

<h2>Plugins e Modularização</h2>

<p>Plugins em Fastify são mecanismos poderosos para encapsular funcionalidade, compartilhar código entre rotas e organizar sua aplicação em módulos reutilizáveis. Diferentemente de middleware tradicional em Express, plugins no Fastify são primeira classe no framework, com suporte nativo a async/await e escopo claro.</p>

<h3>Criando Plugins Simples</h3>

<p>Um plugin é uma função que recebe a instância do Fastify e opções, depois registra rotas, schemas ou decora a instância:</p>

<pre><code class="language-typescript">import { FastifyInstance, FastifyPluginOptions } from &#039;fastify&#039;;

// Define uma interface para as opções do plugin

interface UserPluginOptions {

prefix?: string;

}

// Cria um plugin que encapsula rotas de usuário

export const userRoutes = async (

fastify: FastifyInstance,

options: FastifyPluginOptions &amp; UserPluginOptions

) =&gt; {

const userSchema = {

body: {

type: &#039;object&#039;,

required: [&#039;name&#039;, &#039;email&#039;],

properties: {

name: { type: &#039;string&#039; },

email: { type: &#039;string&#039;, format: &#039;email&#039; },

},

},

} as const;

fastify.post(&#039;/users&#039;, { schema: userSchema }, async (request, reply) =&gt; {

const { name, email } = request.body;

return { id: &#039;1&#039;, name, email };

});

fastify.get(&#039;/users/:id&#039;, async (request, reply) =&gt; {

const { id } = request.params as { id: string };

return { id, name: &#039;João&#039;, email: &#039;joao@example.com&#039; };

});

};

// Registra o plugin na aplicação principal

fastify.register(userRoutes, { prefix: &#039;/api&#039; });</code></pre>

<p>Agora suas rotas estarão disponíveis em <code>/api/users</code> e <code>/api/users/:id</code>. A principal vantagem é organização: cada domínio de negócio (usuários, produtos, pedidos) pode ser um plugin separado, facilitando manutenção em projetos grandes.</p>

<h3>Composição de Plugins</h3>

<p>Plugins podem registrar outros plugins, criando uma hierarquia de funcionalidade:</p>

<pre><code class="language-typescript">// Plugin que adiciona autenticação

export const authPlugin = async (fastify: FastifyInstance) =&gt; {

fastify.decorate(&#039;authenticate&#039;, async (request, reply) =&gt; {

const token = request.headers.authorization?.split(&#039; &#039;)[1];

if (!token || token !== &#039;valid-token-123&#039;) {

throw new Error(&#039;Token inválido&#039;);

}

});

};

// Plugin que usa autenticação

export const protectedRoutes = async (fastify: FastifyInstance) =&gt; {

fastify.register(authPlugin);

fastify.get(

&#039;/admin&#039;,

{

onRequest: [fastify.authenticate],

},

async (request, reply) =&gt; {

return { message: &#039;Dados sensíveis&#039; };

}

);

};

fastify.register(protectedRoutes, { prefix: &#039;/api&#039; });</code></pre>

<p>Este padrão permite reutilização: o <code>authPlugin</code> pode ser aplicado a múltiplos conjuntos de rotas sem duplicação de código.</p>

<h2>Types e Type Safety Avançado</h2>

<p>TypeScript é mais do que adicionar tipos — é sobre desenhar sua aplicação com segurança. Em Fastify, você pode tipar praticamente tudo: rotas, schemas, plugins, decoradores e até hooks. Isso cria um círculo virtuoso onde o compilador TypeScript previne erros antes do código chegar à produção.</p>

<h3>Tipagem Completa de Rotas</h3>

<p>O Fastify permite que você defina tipos genéricos para request, reply, body, params e query:</p>

<pre><code class="language-typescript">import { FastifyInstance, FastifyRequest, FastifyReply } from &#039;fastify&#039;;

interface GetUserParams {

id: string;

}

interface GetUserQuery {

includeProfile?: string;

}

interface UserResponse {

id: string;

name: string;

email: string;

}

fastify.get&lt;{

Params: GetUserParams;

Querystring: GetUserQuery;

Reply: UserResponse;

}&gt;(

&#039;/users/:id&#039;,

async (request: FastifyRequest, reply: FastifyReply) =&gt; {

const { id } = request.params; // id é tipado como string

const { includeProfile } = request.query; // includeProfile é tipado como string | undefined

if (includeProfile === &#039;true&#039;) {

// Lógica customizada

}

return {

id,

name: &#039;Maria&#039;,

email: &#039;maria@example.com&#039;,

};

}

);</code></pre>

<p>Se você tentar acessar <code>request.params.age</code> (que não existe em <code>GetUserParams</code>), TypeScript reclamará em tempo de compilação. Isso é especialmente valioso em equipes grandes onde múltiplas pessoas modificam o código.</p>

<h3>Decoradores Tipados</h3>

<p>Fastify permite decorar a instância com propriedades customizadas. Mantenha-as tipadas:</p>

<pre><code class="language-typescript">import &#039;fastify&#039;;

declare module &#039;fastify&#039; {

interface FastifyInstance {

db: {

getUser(id: string): Promise&lt;{ id: string; name: string }&gt;;

};

}

}

// Plugin que adiciona o decorador

export const dbPlugin = async (fastify: FastifyInstance) =&gt; {

fastify.decorate(&#039;db&#039;, {

getUser: async (id: string) =&gt; {

// Simula busca em banco

return { id, name: &#039;João&#039; };

},

});

};

fastify.register(dbPlugin);

fastify.get(&#039;/users/:id&#039;, async (request) =&gt; {

const user = await fastify.db.getUser(request.params.id);

// user é tipado como { id: string; name: string }

return user;

});</code></pre>

<p>Declarar o módulo no TypeScript garante que toda a aplicação &quot;sabe&quot; sobre seu novo decorador, evitando acessos a propriedades inexistentes.</p>

<h3>Hooks Tipados</h3>

<p>Hooks (beforeRequest, afterResponse, etc.) também podem ser tipados para máxima segurança:</p>

<pre><code class="language-typescript">fastify.addHook(&#039;preHandler&#039;, async (request, reply) =&gt; {

// request e reply são automaticamente tipados

const token = request.headers.authorization;

if (!token) {

reply.code(401).send({ error: &#039;Não autorizado&#039; });

}

});

fastify.post&lt;{ Body: { name: string } }&gt;(

&#039;/create&#039;,

async (request, reply) =&gt; {

// Se o token foi validado no hook anterior

return { success: true };

}

);</code></pre>

<p>Cada hook recebe tipos específicos de request e reply, garantindo que você só acesse propriedades que existem.</p>

<h2>Padrões Práticos e Boas Práticas</h2>

<h3>Estrutura de Projeto Recomendada</h3>

<p>Organize seu projeto de forma escalável desde o início:</p>

<pre><code>src/

├── plugins/

│ ├── auth.ts

│ ├── database.ts

│ └── validation.ts

├── routes/

│ ├── users.ts

│ ├── products.ts

│ └── orders.ts

├── schemas/

│ └── user.schema.ts

├── types/

│ └── index.ts

├── server.ts

└── app.ts</code></pre>

<p>Separe schemas em um arquivo dedicado:</p>

<pre><code class="language-typescript">// src/schemas/user.schema.ts

export const createUserSchema = {

body: {

type: &#039;object&#039;,

required: [&#039;name&#039;, &#039;email&#039;],

properties: {

name: { type: &#039;string&#039;, minLength: 3 },

email: { type: &#039;string&#039;, format: &#039;email&#039; },

},

},

} as const;

export const userResponseSchema = {

type: &#039;object&#039;,

properties: {

id: { type: &#039;string&#039; },

name: { type: &#039;string&#039; },

email: { type: &#039;string&#039; },

},

} as const;</code></pre>

<p>Sua aplicação principal fica limpa:</p>

<pre><code class="language-typescript">// src/app.ts

import Fastify from &#039;fastify&#039;;

import { userRoutes } from &#039;./routes/users&#039;;

import { authPlugin } from &#039;./plugins/auth&#039;;

export const createApp = async () =&gt; {

const fastify = Fastify({ logger: true });

await fastify.register(authPlugin);

await fastify.register(userRoutes, { prefix: &#039;/api&#039; });

return fastify;

};

// src/server.ts

import { createApp } from &#039;./app&#039;;

const main = async () =&gt; {

const app = await createApp();

await app.listen({ port: 3000, host: &#039;0.0.0.0&#039; });

};

main().catch(console.error);</code></pre>

<h3>Tratamento de Erros Consistente</h3>

<p>Defina um padrão para erros e use-o em toda a aplicação:</p>

<pre><code class="language-typescript">export class ApiError extends Error {

constructor(

public code: number,

public message: string,

public details?: unknown

) {

super(message);

this.name = &#039;ApiError&#039;;

}

}

export const errorHandler = async (

error: Error,

request: FastifyRequest,

reply: FastifyReply

) =&gt; {

if (error instanceof ApiError) {

return reply.code(error.code).send({

error: {

message: error.message,

details: error.details,

},

});

}

// Erro genérico

reply.code(500).send({

error: {

message: &#039;Erro interno do servidor&#039;,

},

});

};

fastify.setErrorHandler(errorHandler);

// Uso

fastify.get(&#039;/users/:id&#039;, async (request) =&gt; {

const user = await findUser(request.params.id);

if (!user) {

throw new ApiError(404, &#039;Usuário não encontrado&#039;, {

id: request.params.id,

});

}

return user;

});</code></pre>

<p>Todos os seus endpoints utilizam o mesmo formato de erro, melhorando a experiência de quem consome sua API.</p>

<h2>Conclusão</h2>

<p>Ao dominar Fastify com TypeScript, você constrói APIs que são simultaneamente rápidas, seguras e mantíveis. Três pontos consolidam esse aprendizado: primeiro, <strong>schemas JSON Schema não são apenas validação</strong>, são a especificação viva da sua API, mantendo documentação atualizada automaticamente; segundo, <strong>plugins são a unidade fundamental de organização</strong>, permitindo composição limpa de funcionalidade sem duplicação; terceiro, <strong>type safety com TypeScript previne classes inteiras de bugs</strong>, desde erros de digitação até acessos a propriedades inexistentes, economizando inúmeras horas de debugging em produção.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.fastify.io/" target="_blank" rel="noopener noreferrer">Documentação Oficial do Fastify</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Type System</a></li>

<li><a href="https://json-schema.org/" target="_blank" rel="noopener noreferrer">JSON Schema Specification</a></li>

<li><a href="https://www.fastify.io/docs/latest/Guides/Plugins-Guide/" target="_blank" rel="noopener noreferrer">Fastify Plugins Guide</a></li>

<li><a href="https://www.packtpub.com/" target="_blank" rel="noopener noreferrer">Building Scalable Node.js Applications with TypeScript - Packt Publishing</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

React com TypeScript: Tipando Props, State e Eventos na Prática
React com TypeScript: Tipando Props, State e Eventos na Prática

Introdução: Por que Tipificar em React com TypeScript React é uma biblioteca...

Guia Completo de Tipos Primitivos, Literais e Type Inference em TypeScript
Guia Completo de Tipos Primitivos, Literais e Type Inference em TypeScript

Tipos Primitivos em TypeScript Os tipos primitivos são a base de qualquer pro...

Como Usar Abstract Classes e Métodos Abstratos em TypeScript em Produção
Como Usar Abstract Classes e Métodos Abstratos em TypeScript em Produção

O que são Classes Abstratas em TypeScript? Uma classe abstrata é um modelo ou...