Building a REST API with Claude Code | A Practical Beginner's Guide
Learn REST API fundamentals with Claude Code. A hands-on guide covering endpoint design, validation, and error handling — all with copy-paste ready code.
“I have no idea where to start with REST APIs” — that was me at the beginning too.
Reading documentation felt too abstract. I couldn’t figure out what I was actually supposed to build. Then I started using Claude Code and realized that “build something that works first, then learn” is the fastest approach.
In this guide, we’ll go from zero REST API knowledge to a fully working API — step by step, together with Claude Code. Every code snippet here is copy-paste ready.
What Is a REST API? (In 3 Lines)
A REST API is a convention for manipulating resources (data) on the web via HTTP.
Browser/App → HTTP Request → Server (API)
← JSON Response ←
For a user management API, it looks like this:
| Goal | HTTP Method | URL |
|---|---|---|
| Get all users | GET | /users |
| Get a specific user | GET | /users/123 |
| Create a user | POST | /users |
| Update a user | PUT | /users/123 |
| Delete a user | DELETE | /users/123 |
Once you understand this, you’re ready to start building with Claude Code.
Environment Setup
We’ll use Hono — a lightweight TypeScript web framework. It’s more type-safe than Express and works great with Claude Code.
mkdir my-first-api
cd my-first-api
npm init -y
npm install hono
npm install -D typescript @types/node ts-node
npx tsc --init
Create src/index.ts and ask Claude Code:
claude -p "
Create a REST API boilerplate using Hono in src/index.ts.
- Start server on port 3000
- Add a GET /health endpoint for health checks
- Return JSON responses
Run command: npx ts-node src/index.ts
"
Claude Code will generate something like this:
// src/index.ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";
const app = new Hono();
app.get("/health", (c) => {
return c.json({ status: "ok", timestamp: new Date().toISOString() });
});
serve({ fetch: app.fetch, port: 3000 }, () => {
console.log("Server running at http://localhost:3000");
});
npx ts-node src/index.ts
# → Server running at http://localhost:3000
curl http://localhost:3000/health
# → {"status":"ok","timestamp":"2026-04-27T07:30:00.000Z"}
Did it work? That’s your starting point for REST APIs.
Step 1: Build Basic CRUD
Next, let’s build an API to manage a Todo list. Ask Claude Code:
claude -p "
Add Todo CRUD endpoints to src/index.ts.
- GET /todos — List all todos
- GET /todos/:id — Get one todo
- POST /todos — Create (title: string required)
- PUT /todos/:id — Update (title: string)
- DELETE /todos/:id — Delete
Manage data in an in-memory array.
Todo type: { id: string, title: string, done: boolean, createdAt: string }
Generate id with crypto.randomUUID()
"
Generated code:
import { Hono } from "hono";
import { serve } from "@hono/node-server";
const app = new Hono();
// In-memory data store
type Todo = { id: string; title: string; done: boolean; createdAt: string };
let todos: Todo[] = [];
// Get all todos
app.get("/todos", (c) => c.json(todos));
// Get one todo
app.get("/todos/:id", (c) => {
const todo = todos.find((t) => t.id === c.req.param("id"));
if (!todo) return c.json({ error: "Not found" }, 404);
return c.json(todo);
});
// Create
app.post("/todos", async (c) => {
const body = await c.req.json();
if (!body.title) return c.json({ error: "title is required" }, 400);
const todo: Todo = {
id: crypto.randomUUID(),
title: body.title,
done: false,
createdAt: new Date().toISOString(),
};
todos.push(todo);
return c.json(todo, 201);
});
// Update
app.put("/todos/:id", async (c) => {
const idx = todos.findIndex((t) => t.id === c.req.param("id"));
if (idx === -1) return c.json({ error: "Not found" }, 404);
const body = await c.req.json();
todos[idx] = { ...todos[idx], ...body };
return c.json(todos[idx]);
});
// Delete
app.delete("/todos/:id", (c) => {
const idx = todos.findIndex((t) => t.id === c.req.param("id"));
if (idx === -1) return c.json({ error: "Not found" }, 404);
todos.splice(idx, 1);
return c.json({ message: "Deleted" });
});
serve({ fetch: app.fetch, port: 3000 }, () => {
console.log("Server running at http://localhost:3000");
});
Try it out:
# Create a Todo
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": "Try Claude Code"}'
# Get all todos
curl http://localhost:3000/todos
# Delete (use the id returned above)
curl -X DELETE http://localhost:3000/todos/<id>
Step 2: Add Validation
“An empty title still gets created” or “Any string works as an ID” — validation prevents these issues. Ask Claude Code:
claude -p "
Add validation using zod for POST /todos and PUT /todos/:id.
- title: string, 1-100 characters
- done: (PUT only) boolean
Return 400 error with a specific error message on validation failure
"
npm install zod
Claude Code adds zod schemas:
import { z } from "zod";
const CreateTodoSchema = z.object({
title: z.string().min(1, "Title must be at least 1 character").max(100, "Title must be 100 characters or less"),
});
const UpdateTodoSchema = z.object({
title: z.string().min(1).max(100).optional(),
done: z.boolean().optional(),
});
// POST /todos (with validation)
app.post("/todos", async (c) => {
const body = await c.req.json().catch(() => ({}));
const result = CreateTodoSchema.safeParse(body);
if (!result.success) {
return c.json({ error: result.error.flatten().fieldErrors }, 400);
}
const todo: Todo = {
id: crypto.randomUUID(),
title: result.data.title,
done: false,
createdAt: new Date().toISOString(),
};
todos.push(todo);
return c.json(todo, 201);
});
Check validation errors:
# Create without title → should error
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{}'
# → {"error":{"title":["Title must be at least 1 character"]}}
Step 3: Unify Error Handling
Right now, each endpoint returns errors in different formats. Let Claude Code standardize them:
claude -p "
Unify error handling.
- Common error response format: { error: { code: string, message: string } }
- 404: NOT_FOUND
- 400: VALIDATION_ERROR
- 500: INTERNAL_SERVER_ERROR
Implement a global error handler using Hono's onError
"
// Error type definition
class AppError extends Error {
constructor(
public code: string,
public message: string,
public statusCode: number = 400
) {
super(message);
}
}
// Global error handler
app.onError((err, c) => {
if (err instanceof AppError) {
return c.json(
{ error: { code: err.code, message: err.message } },
err.statusCode as any
);
}
console.error(err);
return c.json(
{ error: { code: "INTERNAL_SERVER_ERROR", message: "An unexpected error occurred" } },
500
);
});
// Usage
app.get("/todos/:id", (c) => {
const todo = todos.find((t) => t.id === c.req.param("id"));
if (!todo) throw new AppError("NOT_FOUND", "Todo not found", 404);
return c.json(todo);
});
Step 4: Auto-generate Swagger UI Documentation
Once you’ve built your API, you need docs. Claude Code can set it up in minutes:
claude -p "
Use @hono/swagger-ui and @hono/zod-openapi to
add Swagger UI at /docs.
Add OpenAPI schemas to the existing endpoints.
"
npm install @hono/swagger-ui @hono/zod-openapi
Once done, visit http://localhost:3000/docs to see your API documentation.
The Claude Code API Development Workflow (Summary)
Here’s the actual flow I use day-to-day:
1. Tell Claude Code "I need an API that does X"
↓
2. Review and test the generated code
↓
3. Iterate: "Fix this", "Add validation", etc.
↓
4. git commit when tests pass
My first stumbling block (real experience)
When I built my first API, I skipped error handling entirely and had to add it all later — which took forever. Now I just add “include error handling” to my prompt upfront, and the code comes out solid from the start. The trick with Claude Code is giving all requirements at once from the beginning.
3 Common Pitfalls
Pitfall 1: Ignoring JSON parse errors
// ❌ Errors here go unnoticed
const body = await c.req.json();
// ✅ Catch parse failures
const body = await c.req.json().catch(() => null);
if (!body) return c.json({ error: "Invalid JSON" }, 400);
Pitfall 2: Confusing 404 and 400
400: The request itself is wrong (validation error, missing required field)
404: The resource doesn't exist (ID not found)
422: Request format is correct but can't be processed
Ask Claude Code “should I return 404 or 400 here?” and it will give you the right answer.
Pitfall 3: Storing everything in memory
The code in this guide uses an in-memory array for demo purposes. Data disappears when you restart the server. In production you’ll need a database like PostgreSQL or MongoDB. Just tell Claude Code “store this in SQLite instead of memory” and it’ll handle the migration.
Next Steps
Once you have the REST API basics down, try these:
| Step | What to Do |
|---|---|
| Database | SQLite → PostgreSQL (with Prisma) |
| Authentication | Add JWT token auth |
| Testing | Write API tests with vitest |
| Deploy | Publish to Vercel / Cloudflare Workers |
For all of these, just tell Claude Code “add X” and the foundation will be ready in minutes. Build something that works first — that’s the fastest path forward.
Related Articles
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
Free PDF: Claude Code Cheatsheet in 5 Minutes
Just enter your email and we'll send you the single-page A4 cheatsheet right away.
We handle your data with care and never send spam.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
Complete Beginner's Guide to Claude Code 2026 | 7 Steps from Zero to Production-Ready
A complete beginner's guide for first-time Claude Code users. From installation to integrating it into your real development workflow — covering every pitfall Masa ran into when starting out.
Blazing-Fast REST API Design, Implementation & Testing with Claude Code | From OpenAPI Spec to Production
Learn how to develop REST APIs end-to-end with Claude Code — from OpenAPI spec generation to production-ready TypeScript code. Covers Hono/Express/Fastify, zod validation, and vitest test generation with working code examples.
Claude Code vs Gemini CLI 2026 Deep Comparison | How Does Google's AI Stack Up?
A hands-on comparison of Claude Code and Gemini CLI by DX engineer Masa. Covers pricing, autonomy, context window, and ecosystem differences. Includes a decision flowchart to help you choose.