Tips & Tricks

Projete, implemente e teste APIs REST em alta velocidade com Claude Code | Da especificação OpenAPI à produção

Aprenda a desenvolver APIs REST do início ao fim com Claude Code: da geração de especificação OpenAPI ao código TypeScript pronto para produção. Inclui Hono, validação com zod, geração de testes vitest e exemplos de código funcionais.

Por que Claude Code é ideal para o design de APIs REST

O desenvolvimento de APIs REST é repleto de padrões repetitivos: definições de endpoints, validação, tratamento de erros e testes — tudo escrito manualmente. É difícil manter o foco na lógica de negócio enquanto se lida com esse boilerplate.

Sou Masa, engenheiro de DX responsável pela modernização de APIs em sistemas internos. Antes do Claude Code, meu custo padrão por endpoint era: “30 min de design → 2 h de implementação → 1 h de testes”. Após integrar o Claude Code completamente: 10 min de design → 30 min de implementação → 15 min de testes.

3 razões pelas quais Claude Code se destaca no desenvolvimento de APIs

  1. Compreende especificações OpenAPI como contexto — cole o YAML e ele gera código conforme a spec instantaneamente
  2. Domina o sistema de tipos do TypeScript — produz código type-safe desde o início
  3. Conhece uma ampla gama de padrões de teste — gera testes de happy path, caminho de erro e casos extremos em uma única execução

1. Gerar automaticamente uma especificação OpenAPI com Claude Code

Exemplo de prompt

Gere uma especificação OpenAPI 3.1 (YAML) para os seguintes requisitos.

Recurso: Task (gerenciamento de tarefas)
Campos:
  - id: UUID
  - title: string (1–200 caracteres)
  - description: string (opcional)
  - status: "todo" | "in_progress" | "done"
  - priority: "low" | "medium" | "high" (padrão: medium)
  - assignee_id: UUID (opcional)
  - due_date: formato ISO8601 (opcional)
  - created_at: formato ISO8601
  - updated_at: formato ISO8601

Endpoints:
  - GET /tasks (lista com paginação)
  - GET /tasks/:id
  - POST /tasks
  - PUT /tasks/:id (atualização completa)
  - PATCH /tasks/:id (atualização parcial)
  - DELETE /tasks/:id

Formato de resposta: { data, meta, errors }
Códigos de erro: 400, 401, 403, 404, 422, 500
Autenticação: Bearer Token (cabeçalho Authorization)

YAML OpenAPI gerado (trecho)

openapi: 3.1.0
info:
  title: Task Management API
  version: 1.0.0
  description: Especificação OpenAPI para a API de Gerenciamento de Tarefas

servers:
  - url: https://api.example.com/v1
    description: Produção
  - url: http://localhost:3000/v1
    description: Desenvolvimento

components:
  schemas:
    Task:
      type: object
      required: [id, title, status, priority, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 200
        status:
          type: string
          enum: [todo, in_progress, done]
        priority:
          type: string
          enum: [low, medium, high]
          default: medium

2. Implementação type-safe de handlers com Hono

// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { taskRouter } from "./routes/tasks";

const app = new Hono();

// Middleware
app.use("*", cors());
app.use("*", logger());
app.use("*", prettyJSON());

// Registro do router
app.route("/v1/tasks", taskRouter);

// Handler global de erros
app.onError((err, c) => {
  console.error(err);
  return c.json(
    { data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "Ocorreu um erro interno do servidor" }] },
    500
  );
});

export default app;

Implementação do router de tarefas

// src/routes/tasks.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { TaskCreateSchema, TaskUpdateSchema, TaskPatchSchema, TaskQuerySchema } from "../schemas/task";
import { TaskService } from "../services/taskService";

export const taskRouter = new Hono();
const taskService = new TaskService();

// Listar tarefas
taskRouter.get("/", zValidator("query", TaskQuerySchema), async (c) => {
  const query = c.req.valid("query");
  const result = await taskService.findAll(query);
  return c.json({
    data: result.tasks,
    meta: { total: result.total, page: query.page, per_page: query.per_page },
    errors: null,
  });
});

// Obter tarefa por ID
taskRouter.get("/:id", async (c) => {
  const id = c.req.param("id");
  const task = await taskService.findById(id);
  if (!task) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Tarefa não encontrada", field: "id" }] },
      404
    );
  }
  return c.json({ data: task, meta: null, errors: null });
});

// Criar tarefa
taskRouter.post("/", zValidator("json", TaskCreateSchema), async (c) => {
  const body = c.req.valid("json");
  const task = await taskService.create(body);
  return c.json({ data: task, meta: null, errors: null }, 201);
});

// Excluir tarefa
taskRouter.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const deleted = await taskService.delete(id);
  if (!deleted) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Tarefa não encontrada", field: "id" }] },
      404
    );
  }
  return c.json({ data: null, meta: null, errors: null }, 204);
});

3. Geração automática de esquemas de validação Zod

// src/schemas/task.ts
import { z } from "zod";

const TaskStatus = z.enum(["todo", "in_progress", "done"]);
const TaskPriority = z.enum(["low", "medium", "high"]);

export const TaskSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1, "O título deve ter pelo menos 1 caractere").max(200, "O título não pode exceder 200 caracteres"),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

export const TaskCreateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().optional(),
  status: TaskStatus.default("todo"),
  priority: TaskPriority.default("medium"),
  assignee_id: z.string().uuid().optional(),
  due_date: z.string().datetime().optional(),
});

export const TaskUpdateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
});

export const TaskPatchSchema = TaskUpdateSchema.partial();

export const TaskQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  per_page: z.coerce.number().int().min(1).max(100).default(20),
  status: TaskStatus.optional(),
  priority: TaskPriority.optional(),
});

export type Task = z.infer<typeof TaskSchema>;
export type TaskCreate = z.infer<typeof TaskCreateSchema>;
export type TaskQuery = z.infer<typeof TaskQuerySchema>;

Comparação antes/depois

MétricaAntes do Claude CodeDepois do Claude Code
Tempo de definição de esquemas30–45 min por endpoint5–10 min (incluindo prompt)
Taxa de erros de tipoFrequente nas fases finaisQuase zero desde o início
Validações esquecidasApontadas em cada revisão de códigoCasos extremos cobertos automaticamente
Divergência spec/códigoDesatualizada em 2–3 semanasSempre sincronizada (gerada da spec)

4. Geração automática de testes com Vitest

// src/routes/tasks.test.ts
import { describe, it, expect, vi } from "vitest";
import app from "../index";
import { TaskService } from "../services/taskService";

vi.mock("../services/taskService");

const mockTask = {
  id: "550e8400-e29b-41d4-a716-446655440000",
  title: "Tarefa de teste",
  description: "Uma tarefa para testes",
  status: "todo" as const,
  priority: "medium" as const,
  assignee_id: null,
  due_date: null,
  created_at: "2026-04-25T00:00:00.000Z",
  updated_at: "2026-04-25T00:00:00.000Z",
};

describe("GET /v1/tasks", () => {
  it("happy path: retorna a lista de tarefas", async () => {
    vi.mocked(TaskService.prototype.findAll).mockResolvedValue({ tasks: [mockTask], total: 1 });
    const res = await app.request("/v1/tasks", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.data).toHaveLength(1);
  });

  it("erro: per_page excede o máximo", async () => {
    const res = await app.request("/v1/tasks?per_page=101", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(400);
  });
});

describe("POST /v1/tasks", () => {
  it("happy path: cria uma tarefa", async () => {
    vi.mocked(TaskService.prototype.create).mockResolvedValue(mockTask);
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "Nova tarefa" }),
    });
    expect(res.status).toBe(201);
  });

  it("erro: título vazio", async () => {
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "" }),
    });
    expect(res.status).toBe(400);
  });
});

5. Padrões de tratamento de erros

// src/lib/errors.ts
export class AppError extends Error {
  constructor(
    public readonly code: string,
    public readonly message: string,
    public readonly statusCode: number,
    public readonly field?: string
  ) {
    super(message);
    this.name = "AppError";
  }
}

export const Errors = {
  notFound: (resource: string) => new AppError("NOT_FOUND", `${resource} não encontrado`, 404),
  unauthorized: () => new AppError("UNAUTHORIZED", "Autenticação necessária", 401),
  forbidden: () => new AppError("FORBIDDEN", "Você não tem permissão para acessar este recurso", 403),
  conflict: (message: string) => new AppError("CONFLICT", message, 409),
  internal: () => new AppError("INTERNAL_ERROR", "Ocorreu um erro interno do servidor", 500),
};

3 armadilhas comuns para evitar

Armadilha 1: Validação de UUID ausente

Claude Code às vezes trata o parâmetro de path :id como uma string simples. Sem validação de formato UUID, consultas malformadas podem chegar ao banco de dados.

// ERRADO: string bruta enviada diretamente ao BD
const task = await db.findById(c.req.param("id"));

// CORRETO: validar o formato UUID primeiro
const idSchema = z.string().uuid("UUID inválido");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
  return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "Formato de ID inválido" }] }, 400);
}
const task = await db.findById(parsed.data);

Armadilha 2: Validação do Content-Type ausente

Para requisições PUT/POST sem Content-Type: application/json, o Hono trata o body como objeto vazio. A validação do zod passa e dados vazios são salvos.

// src/middleware/contentType.ts
import { createMiddleware } from "hono/factory";

export const requireJson = createMiddleware(async (c, next) => {
  if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
    const contentType = c.req.header("Content-Type");
    if (!contentType?.includes("application/json")) {
      return c.json(
        { data: null, meta: null, errors: [{ code: "UNSUPPORTED_MEDIA_TYPE", message: "Content-Type: application/json é necessário" }] },
        415
      );
    }
  }
  await next();
});

Armadilha 3: Problema de fuso horário em campos de data

z.string().datetime() só aceita o formato UTC. Se o frontend enviar 2026-04-25T06:00:00-03:00, será rejeitado.

// ERRADO
due_date: z.string().datetime()

// CORRETO: permitir offset de fuso horário
due_date: z.string().datetime({ offset: true }).optional()

Conclusão

Integrar o Claude Code no desenvolvimento de APIs REST reduz drasticamente o tempo até a produção:

  1. Gerar a especificação OpenAPI primeiro — ela se torna a base de todas as etapas seguintes
  2. Gerar esquemas Zod a partir da spec — escrevê-los manualmente causa divergências
  3. Solicitar explicitamente testes de happy path, caminho de erro e casos extremos — sem instrução, apenas happy paths são escritos
  4. Incluir armadilhas no prompt com antecedência — validação UUID, verificação de Content-Type, tratamento de fuso horário

Desde a adoção desse fluxo de trabalho, o tempo de desenvolvimento por endpoint reduziu em ~70%.

Dito isso, nunca envie o código do Claude Code para produção sem revisão. Segurança e lógica de negócio sempre precisam de olhos humanos. Claude Code é uma máquina de rascunhos rápidos; a responsabilidade final pela qualidade cabe ao desenvolvedor.


Artigos relacionados:

#claude-code #rest-api #openapi #typescript #backend

Leve seu fluxo no Claude Code a outro nível

50 modelos de prompt testados em campo, prontos para colar direto no Claude Code.

Grátis

PDF gratuito: Cheatsheet do Claude Code em 5 minutos

Basta informar seu e-mail e enviamos na hora o cheatsheet em uma página A4.

Cuidamos dos seus dados pessoais e nunca enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro apaixonado por Claude Code. Mantém o claudecode-lab.com, uma mídia tech em 10 idiomas com mais de 2.000 páginas.