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
- Compreende especificações OpenAPI como contexto — cole o YAML e ele gera código conforme a spec instantaneamente
- Domina o sistema de tipos do TypeScript — produz código type-safe desde o início
- 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étrica | Antes do Claude Code | Depois do Claude Code |
|---|---|---|
| Tempo de definição de esquemas | 30–45 min por endpoint | 5–10 min (incluindo prompt) |
| Taxa de erros de tipo | Frequente nas fases finais | Quase zero desde o início |
| Validações esquecidas | Apontadas em cada revisão de código | Casos extremos cobertos automaticamente |
| Divergência spec/código | Desatualizada em 2–3 semanas | Sempre 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:
- Gerar a especificação OpenAPI primeiro — ela se torna a base de todas as etapas seguintes
- Gerar esquemas Zod a partir da spec — escrevê-los manualmente causa divergências
- Solicitar explicitamente testes de happy path, caminho de erro e casos extremos — sem instrução, apenas happy paths são escritos
- 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:
Leve seu fluxo no Claude Code a outro nível
50 modelos de prompt testados em campo, prontos para colar direto no Claude Code.
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.
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.
Artigos relacionados
Guia Completo para Começar com Claude Code 2026 | 7 Passos do Zero ao Uso Profissional
O guia definitivo para quem usa Claude Code pela primeira vez. Da instalação à integração no fluxo de desenvolvimento real — com todos os tropeços que Masa encontrou no começo.
Criando uma REST API com Claude Code | Guia Prático para Iniciantes
Aprenda os fundamentos de REST API com Claude Code. Um guia prático cobrindo design de endpoints, validação e tratamento de erros — com código pronto para copiar.
Claude Code vs Gemini CLI 2026 Comparação Completa | Como a IA do Google se Compara?
Comparação prática de Claude Code e Gemini CLI pelo engenheiro DX Masa. Preços, autonomia, janela de contexto e ecossistema analisados. Inclui fluxograma de decisão para escolher a ferramenta certa.