# 10 Common Mistakes When Building REST APIs (and How to Avoid Them)

REST APIs seem simple-until you try building a real one. Whether you’re designing internal services or public endpoints, mistakes in structure, naming, or error handling can haunt you for months.

![](https://cdn-images-1.medium.com/max/1080/0*mBhKA__jSV1SdBLw align="left")

Photo by [Francisco De Legarreta C.](https://unsplash.com/@francisco_legarreta?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)

### Read this first

* Mistakes are common everywhere. Don’t fret it too much. Never stop learning.
    

Here are 10 common (but fixable) mistakes developers make when building REST APIs-and what to do instead.

### 1\. Using Verbs in Endpoints

REST is resource-oriented. The **HTTP method** already describes the action. If you are using verbs to define your endpoint, it will be redundant.

```typescript
GET /createUser  
POST /deleteOrder
```

You can fix this by using nouns for endpoints and verbs only in HTTP methods.

```typescript
POST /users        // create user  
DELETE /orders/:id // delete order
```

### 2\. Ignoring Status Codes

This confuses clients that rely on status codes for logic (think: frontend, mobile apps, API consumers).

```typescript
res.status(200).json({ error: "User not found" });
```

Utilize the status code based on their meaning. Return appropriate status codes-`200`, `201`, `400`, `404`, `500`... Don't make everything a `200`.

```typescript
res.status(404).json({ error: "User not found" });
```

### 3\. Exposing Internal Errors Directly

From security point of view, it could leak some sensitive information or give clues to intruders.

```typescript
res.status(500).json({ message: err.message }); // might expose stack traces or DB internals
```

Carefully review your implementation. Log errors internally if required.

```typescript
res.status(500).json({ error: "Internal server error" });
myLogger.logError(err); // log the detailed error internally
```

### 4\. Not Versioning Your API

Excluding version route in your endpoint can lead to potential future issues

```typescript
GET /users
// How do you release breaking changes later?
```

A simple way to fix it would be include version info in your endpoint. Make this habit from day one.

```typescript
GET /v1/users
```

Alternatively, you could use headers (`Accept-Version`).

### 5\. Overloading 200 OK for Everything

This sounds familiar to an earlier mistake. Using 200 everytime might be syntactically OK but it gives a false confidence to clients.

```typescript
res.status(200).json({ success: false, error: "Bad input" });
```

Be semantically correct as well. Don’t lie with 200.

```typescript
res.status(400).json({ error: "Bad input" });
```

### 6\. Inconsistent Naming and Pluralization

```typescript
GET /user  
GET /orders  
POST /order
```

There’s no strict rule for it but assume plural noun to be the norm to make your life (and your future-self’s life) easier.

```typescript
GET /users  
GET /orders  
POST /orders
```

### 7\. Cramming Too Much Into One Endpoint

The most common reason for an endpoint to grow insanely huge-it’s convenient.

```typescript
GET /users?includeOrders=true&includeComments=true&includeProfile=true
```

There are many ways to fix this. Let’s go with a basic idea of breaking down the endpoint by resources.

```typescript
GET `/users/:id`
GET `/users/:id/orders`
GET `/users/:id/profile`
```

### 8\. No Rate Limiting or Throttling

A classic but costly mistake. Anyone can hit your API a few thousand times a second and you won’t notice until the server dies. (The intention is not always with a DDoS intent… but it could be!)

Always rate-limit your APIs.

```typescript
// Simple example with middleware
import rateLimit from 'express-rate-limit';
app.use(rateLimit({
  windowMs: 1 * 60 * 1000,
  max: 100,
}));
```

### 9\. Trusting the Client a bit too much (Poor Input Validation)

Clients don’t always send what you expect-sometimes due to bugs, sometimes due to… creative users. This leads to garbage data, crashes, or worse-security issues.

```typescript
app.post('/users', (req, res) => {
  const { email, age } = req.body; // assumes everything's fine
  // ...
});
```

Always validate your input. There are various libraries to make it easy for you — Zod, Yup, Joi, etc.

```typescript
import { z } from 'zod';
const schema = z.object({
  email: z.string().email(),
  age: z.number().int().positive(),
});
app.post('/users', (req, res) => {
  const parsed = schema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: parsed.error });
  // ...
});
```

### 10\. Assuming Everyone Speaks JSON

Most APIs default to JSON responses-and that’s fine. But **assuming** it’s the *only* format clients ever need can be shortsighted.

```typescript
res.send('Success'); // No content type set
```

Or:

```typescript
res.json({ message: "Success" }); // even when the client asked for CSV or XML
```

* You **don’t negotiate the correct** `Content-Type`, so clients may misinterpret your response.
    
* Some clients (like CLI tools, legacy systems, BI tools) might expect other formats like **CSV** or **XML**.
    
* Debugging becomes painful when the frontend receives HTML error pages instead of structured JSON errors.
    

I think requesting something apart from JSON is a rare problem but let’s consider a possible fix.

Utilize `Accept` and `Content-Type` headers. Check and respect the `Accept` header sent by the client. Here's a TypeScript + Express example that supports both `application/json` and `text/csv`:

```typescript
app.get('/reports', (req, res) => {
  const data = [
    { id: 1, name: "Hakuna" },
    { id: 2, name: "Matata" },
  ];
const accept = req.headers['accept'];
  if (accept?.includes('text/csv')) {
    const csv = data.map(row => `${row.id},${row.name}`).join('\n');
    res.setHeader('Content-Type', 'text/csv');
    return res.status(200).send(csv);
  }
  if (accept?.includes('application/json') || !accept) {
    res.setHeader('Content-Type', 'application/json');
    return res.status(200).json(data);
  }
  return res.status(406).send('Not Acceptable');
});
```

* Use the `Accept` header to determine what the client wants.
    
* Return a `406 Not Acceptable` if the requested format isn't supported.
    
* Communicate data type on both sides with `Content-Type` header.
    
* Keep JSON as your default, but make future extensibility easy.
    

### One Last Thing: Undocumented APIs Are Invisible

If no one knows how to use your API, it might as well not exist.

Use tools like OpenAPI, Swagger UI, or Redoc to generate and maintain clear, up-to-date docs automatically.

### Final Thoughts

Designing a good REST API isn’t just about writing endpoints-it’s about building clear, predictable contracts between systems. These mistakes are common, but fixing them early can save you from weeks of work later.

Start simple. Make things explicit. Keep improving.

**Have you stumbled on any of these?** Drop a comment-I’d love to hear it.
