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

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

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:

npm init -y
npm install fastify
npm install -D typescript @types/node ts-node tsx
npx tsc --init

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

import Fastify from 'fastify';

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

fastify.get('/', async (request, reply) => {
  return { message: 'Olá, Fastify!' };
});

const start = async () => {
  try {
    await fastify.listen({ port: 3000 });
    console.log('Servidor rodando em http://localhost:3000');
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();

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

Schemas e Validação de Dados

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.

Definindo Schemas com JSON Schema

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

import Fastify, { FastifyInstance } from 'fastify';

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

// Define o schema de entrada (body)
const createUserSchema = {
  body: {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 3 },
      email: { type: 'string', format: 'email' },
      age: { type: 'integer', minimum: 0 },
    },
  },
  // Define o schema de saída (response)
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        email: { type: 'string' },
        createdAt: { type: 'string' },
      },
    },
  },
};

interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
  createdAt: string;
}

fastify.post<{ Body: { name: string; email: string; age?: number } }>(
  '/users',
  { schema: createUserSchema },
  async (request, reply) => {
    // 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 });

Quando alguém envia um POST sem o campo name, ou com um email 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.

Type Safety com Interfaces

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

// 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: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 3 },
      email: { type: 'string', format: 'email' },
      age: { type: 'integer', minimum: 0 },
    },
  } as const,
};

fastify.post<{ Body: CreateUserRequest; Reply: UserResponse }>(
  '/users',
  { schema: userSchema },
  async (request, reply) => {
    // request.body é tipado como CreateUserRequest
    // reply é tipado para retornar UserResponse
    return {
      id: '123',
      name: request.body.name,
      email: request.body.email,
      age: request.body.age,
      createdAt: new Date().toISOString(),
    };
  }
);

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.

Plugins e Modularização

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.

Criando Plugins Simples

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

import { FastifyInstance, FastifyPluginOptions } from 'fastify';

// 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 & UserPluginOptions
) => {
  const userSchema = {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string' },
        email: { type: 'string', format: 'email' },
      },
    },
  } as const;

  fastify.post('/users', { schema: userSchema }, async (request, reply) => {
    const { name, email } = request.body;
    return { id: '1', name, email };
  });

  fastify.get('/users/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    return { id, name: 'João', email: 'joao@example.com' };
  });
};

// Registra o plugin na aplicação principal
fastify.register(userRoutes, { prefix: '/api' });

Agora suas rotas estarão disponíveis em /api/users e /api/users/:id. 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.

Composição de Plugins

Plugins podem registrar outros plugins, criando uma hierarquia de funcionalidade:

// Plugin que adiciona autenticação
export const authPlugin = async (fastify: FastifyInstance) => {
  fastify.decorate('authenticate', async (request, reply) => {
    const token = request.headers.authorization?.split(' ')[1];
    if (!token || token !== 'valid-token-123') {
      throw new Error('Token inválido');
    }
  });
};

// Plugin que usa autenticação
export const protectedRoutes = async (fastify: FastifyInstance) => {
  fastify.register(authPlugin);

  fastify.get(
    '/admin',
    {
      onRequest: [fastify.authenticate],
    },
    async (request, reply) => {
      return { message: 'Dados sensíveis' };
    }
  );
};

fastify.register(protectedRoutes, { prefix: '/api' });

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

Types e Type Safety Avançado

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.

Tipagem Completa de Rotas

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

import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

interface GetUserParams {
  id: string;
}

interface GetUserQuery {
  includeProfile?: string;
}

interface UserResponse {
  id: string;
  name: string;
  email: string;
}

fastify.get<{
  Params: GetUserParams;
  Querystring: GetUserQuery;
  Reply: UserResponse;
}>(
  '/users/:id',
  async (request: FastifyRequest, reply: FastifyReply) => {
    const { id } = request.params; // id é tipado como string
    const { includeProfile } = request.query; // includeProfile é tipado como string | undefined

    if (includeProfile === 'true') {
      // Lógica customizada
    }

    return {
      id,
      name: 'Maria',
      email: 'maria@example.com',
    };
  }
);

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

Decoradores Tipados

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

import 'fastify';

declare module 'fastify' {
  interface FastifyInstance {
    db: {
      getUser(id: string): Promise<{ id: string; name: string }>;
    };
  }
}

// Plugin que adiciona o decorador
export const dbPlugin = async (fastify: FastifyInstance) => {
  fastify.decorate('db', {
    getUser: async (id: string) => {
      // Simula busca em banco
      return { id, name: 'João' };
    },
  });
};

fastify.register(dbPlugin);

fastify.get('/users/:id', async (request) => {
  const user = await fastify.db.getUser(request.params.id);
  // user é tipado como { id: string; name: string }
  return user;
});

Declarar o módulo no TypeScript garante que toda a aplicação "sabe" sobre seu novo decorador, evitando acessos a propriedades inexistentes.

Hooks Tipados

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

fastify.addHook('preHandler', async (request, reply) => {
  // request e reply são automaticamente tipados
  const token = request.headers.authorization;

  if (!token) {
    reply.code(401).send({ error: 'Não autorizado' });
  }
});

fastify.post<{ Body: { name: string } }>(
  '/create',
  async (request, reply) => {
    // Se o token foi validado no hook anterior
    return { success: true };
  }
);

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

Padrões Práticos e Boas Práticas

Estrutura de Projeto Recomendada

Organize seu projeto de forma escalável desde o início:

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

Separe schemas em um arquivo dedicado:

// src/schemas/user.schema.ts
export const createUserSchema = {
  body: {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 3 },
      email: { type: 'string', format: 'email' },
    },
  },
} as const;

export const userResponseSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    name: { type: 'string' },
    email: { type: 'string' },
  },
} as const;

Sua aplicação principal fica limpa:

// src/app.ts
import Fastify from 'fastify';
import { userRoutes } from './routes/users';
import { authPlugin } from './plugins/auth';

export const createApp = async () => {
  const fastify = Fastify({ logger: true });

  await fastify.register(authPlugin);
  await fastify.register(userRoutes, { prefix: '/api' });

  return fastify;
};

// src/server.ts
import { createApp } from './app';

const main = async () => {
  const app = await createApp();
  await app.listen({ port: 3000, host: '0.0.0.0' });
};

main().catch(console.error);

Tratamento de Erros Consistente

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

export class ApiError extends Error {
  constructor(
    public code: number,
    public message: string,
    public details?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const errorHandler = async (
  error: Error,
  request: FastifyRequest,
  reply: FastifyReply
) => {
  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: 'Erro interno do servidor',
    },
  });
};

fastify.setErrorHandler(errorHandler);

// Uso
fastify.get('/users/:id', async (request) => {
  const user = await findUser(request.params.id);
  if (!user) {
    throw new ApiError(404, 'Usuário não encontrado', {
      id: request.params.id,
    });
  }
  return user;
});

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

Conclusão

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, schemas JSON Schema não são apenas validação, são a especificação viva da sua API, mantendo documentação atualizada automaticamente; segundo, plugins são a unidade fundamental de organização, permitindo composição limpa de funcionalidade sem duplicação; terceiro, type safety com TypeScript previne classes inteiras de bugs, desde erros de digitação até acessos a propriedades inexistentes, economizando inúmeras horas de debugging em produção.

Referências


Artigos relacionados