DEV Community

Cover image for Stop Writing Spaghetti APIs: A Practical Guide to Clean REST API Design
Teguh Coding
Teguh Coding

Posted on

Stop Writing Spaghetti APIs: A Practical Guide to Clean REST API Design

Stop Writing Spaghetti APIs: A Practical Guide to Clean REST API Design

We've all been there. You join a new project, open up the API docs (if they even exist), and suddenly you're staring into the void. Endpoints like /getUser, /updateUserData2, /fetchOrdersByUserIdAndStatus_v3... it's a horror show.

The truth is, bad API design is one of the most expensive technical debts a team can accumulate. It slows down frontend devs, confuses consumers, and makes maintenance a nightmare. But here's the good news: clean REST API design isn't rocket science. It's a set of principles you can start applying today.

Let's walk through the most common mistakes and how to fix them — with real examples.


1. Name Your Resources, Not Your Actions

The single biggest mistake I see in REST APIs is using verbs in endpoint paths.

# ❌ Bad — RPC-style thinking
GET  /getUsers
POST /createUser
POST /deleteUser
POST /updateUserEmail

# ✅ Good — Resource-oriented
GET    /users
POST   /users
DELETE /users/{id}
PATCH  /users/{id}
Enter fullscreen mode Exit fullscreen mode

REST is built around resources (nouns), not actions (verbs). The HTTP method is the action. Trust it.

Think of your API like a filing cabinet. You don't say "getFolderFromCabinet" — you just open the users drawer and either read, add, update, or remove a file.


2. Use HTTP Methods Correctly

This sounds obvious, but you'd be surprised. Here's the quick reference:

Method Purpose Idempotent?
GET Retrieve resource(s) ✅ Yes
POST Create a new resource ❌ No
PUT Replace a resource entirely ✅ Yes
PATCH Partially update a resource Usually ✅
DELETE Remove a resource ✅ Yes

Idempotent means calling the same request multiple times produces the same result. This matters for retry logic and caching.

A classic mistake: using POST for everything because it "feels safer". It breaks caching, confuses clients, and makes your API unpredictable.


3. Embrace Consistent Status Codes

HTTP status codes are a language. Speak it fluently.

// Express.js example — proper status codes

// ✅ 201 Created when a resource is successfully created
app.post('/users', async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json({ data: user });
});

// ✅ 404 Not Found when resource doesn't exist
app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ 
      error: { message: 'User not found', code: 'USER_NOT_FOUND' } 
    });
  }
  res.status(200).json({ data: user });
});

// ✅ 422 Unprocessable Entity for validation errors
app.post('/users', async (req, res) => {
  const { error } = validateUser(req.body);
  if (error) {
    return res.status(422).json({
      error: { message: 'Validation failed', details: error.details }
    });
  }
  // ...
});
Enter fullscreen mode Exit fullscreen mode

The most commonly misused codes:

  • 200 for everything (even errors!) — stop this immediately
  • 500 when validation fails — that's a 400-level error, not a server error
  • 404 when authorization fails — use 403 Forbidden

4. Structure Your Responses Consistently

Nothing frustrates API consumers more than inconsistency. Sometimes you return { user: {...} }, sometimes { data: {...} }, sometimes just the raw object.

Pick a structure and stick with it:

// A clean, consistent response envelope

// Success
{
  "data": {
    "id": "usr_123",
    "name": "Teguh",
    "email": "teguh@example.com",
    "createdAt": "2026-02-27T02:00:00Z"
  },
  "meta": {
    "requestId": "req_abc456"
  }
}

// Error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is already in use",
    "details": [
      { "field": "email", "message": "Must be unique" }
    ]
  },
  "meta": {
    "requestId": "req_abc456"
  }
}

// Paginated list
{
  "data": [...],
  "pagination": {
    "page": 1,
    "perPage": 20,
    "total": 200,
    "totalPages": 10
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice the requestId in meta? That's gold for debugging. Generate a unique ID per request and log it server-side. When users report issues, they can give you the ID and you can trace exactly what happened.


5. Version Your API from Day One

This is the one mistake that's almost impossible to fix retroactively without breaking things.

# Version in the URL path (most common, easiest)
https://api.yourdomain.com/v1/users
https://api.yourdomain.com/v2/users

# Version in headers (more "pure" REST, harder for consumers)
GET /users
Accept: application/vnd.yourapi.v2+json
Enter fullscreen mode Exit fullscreen mode

URL versioning is less elegant but far more practical. It's obvious, cacheable, and easy to test in a browser.

Start with v1 even if you think your API is perfect. It will change. It always does.


6. Design Nested Resources Thoughtfully

Relationships between resources are tricky. Here's the mental model:

# ✅ Nest when the child resource only makes sense in context of the parent
GET /users/{userId}/orders          # Orders belonging to a user
GET /posts/{postId}/comments        # Comments on a post
POST /posts/{postId}/comments       # Add comment to a post

# ❌ Avoid deep nesting — it gets messy
GET /users/{userId}/orders/{orderId}/items/{itemId}/reviews

# ✅ Better: flatten deep hierarchies
GET /order-items/{itemId}/reviews
Enter fullscreen mode Exit fullscreen mode

A good rule of thumb: maximum 2 levels of nesting. If you need to go deeper, flatten it.


7. Handle Filtering, Sorting, and Pagination Gracefully

# Filtering
GET /orders?status=pending&userId=123

# Sorting (use a consistent convention)
GET /products?sort=price&order=asc
GET /products?sort=-price          # Minus prefix = descending (popular pattern)

# Pagination — cursor-based is better for large datasets
GET /posts?page=1&perPage=20       # Offset-based (simple)
GET /posts?cursor=eyJpZCI6MTIzfQ== # Cursor-based (scalable)

# Field selection (sparse fieldsets)
GET /users?fields=id,name,email    # Return only what's needed
Enter fullscreen mode Exit fullscreen mode

Never return unbounded lists. Always paginate. An endpoint that returns 50,000 records because no one thought to add pagination has probably taken down a production server before.


8. Document as You Build

The best API documentation is one that writes itself. Use OpenAPI (Swagger) specs:

# openapi.yaml — a taste of what self-documenting looks like
openapi: 3.0.0
info:
  title: My Clean API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      summary: Get a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '404':
          description: User not found
Enter fullscreen mode Exit fullscreen mode

Tools like Swagger UI, Redoc, and Scalar can turn this YAML into beautiful interactive docs automatically.


Quick Wins Checklist

Before you ship your next API, run through this:

  • [ ] Resource names are plural nouns (/users, not /user or /getUsers)
  • [ ] HTTP methods are used correctly (no POST for everything)
  • [ ] Status codes actually reflect what happened
  • [ ] Responses follow a consistent envelope structure
  • [ ] API is versioned (/v1/...)
  • [ ] All list endpoints are paginated
  • [ ] Error responses include a machine-readable code field
  • [ ] A requestId is included in every response
  • [ ] OpenAPI spec exists (even if minimal)

Final Thoughts

Clean API design is an act of empathy. Every endpoint you craft is something another developer — maybe future-you — will have to consume and maintain. The extra 20 minutes you spend naming things well and using the right status codes will save hours of confusion down the road.

The best APIs feel intuitive. You can almost guess the endpoint before you look it up. That's the goal.

Start small. Pick one project and apply these principles. Refactor one bad endpoint. Add versioning to one API. The habits compound.

Happy coding! 🚀


Found this helpful? Share it with a teammate who's building APIs. And if you've got your own API design tips, drop them in the comments!

Top comments (0)