Tips & Tricks (Atualizado: 03/06/2026)

Criar sua primeira REST API com Claude Code: CRUD, validação e testes

Crie uma REST API Express com Claude Code: CRUD, validação, erros, testes e código executável.

Criar sua primeira REST API com Claude Code: CRUD, validação e testes

Ao criar sua primeira REST API, o problema raramente é escrever rotas no Express. A parte difícil é saber o que uma API útil precisa ter: URLs claras, métodos HTTP corretos, validação, respostas de erro previsíveis e alguns testes que provem que o código funciona.

Neste guia, você vai construir com Claude Code uma pequena API de Todos usando Express. Você criará o projeto, adicionará endpoints CRUD, validará JSON, retornará códigos de status úteis e testará tudo com curl e o test runner nativo do Node.js. O código mira Node.js 22/24 LTS ou uma versão mais nova.

Se Claude Code ainda é novidade para você, comece pelo guia inicial de Claude Code. Para se aprofundar em testes, leia Claude Code API testing e validação com Zod.

Termos essenciais

Uma REST API é um estilo para trabalhar com recursos via HTTP. Neste artigo, o recurso é um todo. Em termos simples, outro app chama uma URL, envia ou recebe JSON e usa o código de status para entender o resultado.

TermoSignificado simplesExemplo
EndpointURL de entrada da APIGET /todos
MétodoVerbo HTTP que descreve a açãoGET, POST, PUT, DELETE
Código de statusNúmero que informa o resultado200, 201, 400, 404
JSONFormato de texto leve para dados{ "title": "Learn REST" }
ValidaçãoChecar se a entrada é aceitávelRejeitar title vazio
IdempotênciaRepetir a mesma requisição mantém o mesmo estado finalEnviar o mesmo PUT duas vezes

As referências oficiais úteis são métodos HTTP da MDN, códigos de status HTTP da MDN, Express routing, Express error handling e Node.js test runner.

O que vamos construir

A API guarda os Todos em memória. Ao reiniciar o servidor, ela volta ao estado inicial. Isso é proposital: primeiro entendemos o formato REST, depois adicionamos banco de dados.

AçãoMétodo e URLStatus de sucesso
Health checkGET /health200 OK
Listar TodosGET /todos200 OK
Ler um TodoGET /todos/:id200 OK
Criar TodoPOST /todos201 Created
Substituir TodoPUT /todos/:id200 OK
Excluir TodoDELETE /todos/:id204 No Content

Mesmo pequeno, o exemplo encaixa em trabalho real. Primeiro caso: API interna para tarefas, tickets ou revisões. Segundo: backend mock para desenvolver telas em React, Vue ou Astro antes do backend definitivo. Terceiro: API pequena para contato ou newsletter, trocando title por email e message.

Dê critérios claros ao Claude Code

Informe stack, endpoints, erros e verificação. Um prompt vago como “crie uma API” costuma gerar algo que inicia, mas deixa decisões de design sem revisão.

Create a beginner Todo REST API with Express 5 and Node.js ES Modules.

Requirements:
- Provide package.json and server.js
- Add GET /health, GET /todos, GET /todos/:id, POST /todos, PUT /todos/:id, DELETE /todos/:id
- Accept JSON input
- title must be 1 to 120 characters; completed must be boolean
- Return JSON errors for 400, 404, and 500
- Keep PUT idempotent: repeating the same request should not change the final resource state
- Include curl requests and a node:test example

A linha sobre idempotência importa. PUT representa substituição. Se o mesmo corpo for enviado duas vezes, o Todo deve terminar no mesmo estado. Se updatedAt mudar sempre, mesmo sem mudança de dados, o código contradiz a explicação.

Criar o projeto

mkdir claude-rest-api
cd claude-rest-api
npm init -y
npm install express

Substitua package.json:

{
  "name": "claude-rest-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}

server.js

import express from "express";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

const PORT = Number(process.env.PORT ?? 3000);

function createHttpError(status, message, details) {
  const error = new Error(message);
  error.status = status;
  error.details = details;
  return error;
}

function hasOwn(object, key) {
  return Object.prototype.hasOwnProperty.call(object, key);
}

function parseId(rawId) {
  const id = Number(rawId);
  return Number.isInteger(id) && id > 0 ? id : null;
}

function validateTodoInput(body, { partial = false } = {}) {
  const errors = [];

  if (!partial || hasOwn(body, "title")) {
    if (typeof body.title !== "string" || body.title.trim().length === 0) {
      errors.push({ field: "title", message: "title is required" });
    } else if (body.title.trim().length > 120) {
      errors.push({ field: "title", message: "title must be 120 characters or fewer" });
    }
  }

  if (!partial || hasOwn(body, "completed")) {
    if (typeof body.completed !== "boolean") {
      errors.push({ field: "completed", message: "completed must be a boolean" });
    }
  }

  return errors;
}

export function createApp() {
  const app = express();

  let nextId = 3;
  const todos = [
    {
      id: 1,
      title: "Read MDN HTTP status docs",
      completed: false,
      updatedAt: "2026-06-03T00:00:00.000Z"
    },
    {
      id: 2,
      title: "Ask Claude Code to review API errors",
      completed: true,
      updatedAt: "2026-06-03T00:00:00.000Z"
    }
  ];

  app.use(express.json({ limit: "32kb" }));

  app.get("/health", (req, res) => {
    res.json({ status: "ok", uptime: process.uptime() });
  });

  app.get("/todos", (req, res) => {
    res.json({ data: todos, count: todos.length });
  });

  app.get("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const todo = todos.find((item) => item.id === id);
    if (!todo) return next(createHttpError(404, "todo not found"));

    res.json({ data: todo });
  });

  app.post("/todos", (req, res, next) => {
    const errors = validateTodoInput(req.body);
    if (errors.length > 0) {
      return next(createHttpError(400, "invalid request body", errors));
    }

    const todo = {
      id: nextId,
      title: req.body.title.trim(),
      completed: req.body.completed,
      updatedAt: new Date().toISOString()
    };
    nextId += 1;
    todos.push(todo);

    res.status(201).location(`/todos/${todo.id}`).json({ data: todo });
  });

  app.put("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const errors = validateTodoInput(req.body);
    if (errors.length > 0) {
      return next(createHttpError(400, "invalid request body", errors));
    }

    const index = todos.findIndex((item) => item.id === id);
    if (index === -1) return next(createHttpError(404, "todo not found"));

    const previous = todos[index];
    const nextTodo = {
      ...previous,
      title: req.body.title.trim(),
      completed: req.body.completed
    };

    const changed =
      previous.title !== nextTodo.title || previous.completed !== nextTodo.completed;

    todos[index] = {
      ...nextTodo,
      updatedAt: changed ? new Date().toISOString() : previous.updatedAt
    };

    res.json({ data: todos[index] });
  });

  app.delete("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const index = todos.findIndex((item) => item.id === id);
    if (index === -1) return next(createHttpError(404, "todo not found"));

    todos.splice(index, 1);
    res.status(204).send();
  });

  app.use((req, res, next) => {
    next(createHttpError(404, `route not found: ${req.method} ${req.originalUrl}`));
  });

  app.use((err, req, res, next) => {
    const status = Number.isInteger(err.status) && err.status >= 400 ? err.status : 500;
    const body = {
      error: {
        status,
        message: status === 500 ? "Internal Server Error" : err.message
      }
    };

    if (err.details) {
      body.error.details = err.details;
    }

    res.status(status).json(body);
  });

  return app;
}

const currentFile = fileURLToPath(import.meta.url);
const startedFile = process.argv[1] ? resolve(process.argv[1]) : "";

if (startedFile === currentFile) {
  createApp().listen(PORT, () => {
    console.log(`REST API listening on http://localhost:${PORT}`);
  });
}

O essencial é a consistência: sucesso em { data: ... }, erro em { error: ... } e um status code adequado para cada caso.

Testar com curl

npm run dev
curl -i http://localhost:3000/health

curl -i http://localhost:3000/todos

curl -i -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Write API tests","completed":false}'

curl -i -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"","completed":false}'

curl -i -X PUT http://localhost:3000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Read MDN HTTP status docs","completed":true}'

curl -i -X DELETE http://localhost:3000/todos/2

Confira sucesso e falha. Uma API que só funciona no caminho feliz ainda não está pronta para uma interface real.

Adicionar node:test

Crie server.test.js:

import assert from "node:assert/strict";
import test from "node:test";
import { createApp } from "./server.js";

function listen(app) {
  return new Promise((resolve) => {
    const server = app.listen(0, () => resolve(server));
  });
}

async function request(baseUrl, path, options = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    headers: { "Content-Type": "application/json", ...options.headers },
    ...options
  });
  const text = await response.text();
  return {
    response,
    body: text ? JSON.parse(text) : null
  };
}

test("creates, updates, and deletes a todo", async (t) => {
  const server = await listen(createApp());
  t.after(() => server.close());
  const baseUrl = `http://127.0.0.1:${server.address().port}`;

  const created = await request(baseUrl, "/todos", {
    method: "POST",
    body: JSON.stringify({ title: "Test the API", completed: false })
  });
  assert.equal(created.response.status, 201);
  assert.equal(created.body.data.title, "Test the API");

  const id = created.body.data.id;
  const updated = await request(baseUrl, `/todos/${id}`, {
    method: "PUT",
    body: JSON.stringify({ title: "Test the API", completed: true })
  });
  assert.equal(updated.response.status, 200);
  assert.equal(updated.body.data.completed, true);

  const deleted = await request(baseUrl, `/todos/${id}`, { method: "DELETE" });
  assert.equal(deleted.response.status, 204);
});

test("rejects invalid todo input", async (t) => {
  const server = await listen(createApp());
  t.after(() => server.close());
  const baseUrl = `http://127.0.0.1:${server.address().port}`;

  const result = await request(baseUrl, "/todos", {
    method: "POST",
    body: JSON.stringify({ title: "", completed: false })
  });

  assert.equal(result.response.status, 400);
  assert.equal(result.body.error.details[0].field, "title");
});
npm test

Esses testes são pequenos, mas dão segurança. Quando você pedir ao Claude Code para adicionar banco, OpenAPI ou autenticação, peça que mantenha essa suíte passando.

Armadilhas comuns

Não use endpoints como /getTodos. Deixe o método ser o verbo: GET /todos, POST /todos, PUT /todos/:id.

Não retorne sempre 200. Entrada inválida é 400, recurso ausente é 404, criação correta é 201 e exclusão sem corpo é 204.

Não dependa só da validação do frontend. A API pode ser chamada diretamente, então o servidor deve validar.

Não exponha stack traces aos clientes. Registre detalhes no servidor e retorne JSON controlado.

Não quebre a idempotência. Se um PUT muda timestamps ou valores aleatórios a cada chamada idêntica, repetir a requisição não mantém o mesmo estado final.

Próximos passos e CTA

Depois, adicione uma coisa por vez: banco de dados, validação de esquema, documentação da API e autenticação. Para design de erros, veja padrões de error handling com Claude Code.

Para praticar no seu próprio projeto, use a folha gratuita enquanto escreve prompts para Claude Code. Para prompts reutilizáveis, checklists de revisão e modelos CLAUDE.md, veja a biblioteca de produtos Claude Code. Para onboarding de equipe, regras de revisão de API e desenho de permissões, use treinamento e consultoria Claude Code.

Ao testar o conteúdo deste artigo na prática, o maior avanço para iniciantes não foi o CRUD em si. Foi ver sucesso e erro lado a lado: title vazio retorna 400, ID desconhecido retorna 404 e repetir o mesmo PUT preserva o estado final. Assim, REST deixa de ser uma lista de URLs e passa a ser um contrato entre frontend e backend.

#claude-code #rest-api #beginner #typescript #backend #tutorial
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.