API Design Mistakes That Will Haunt You for Years
APIs Are Forever
A bad database schema can be migrated. A bad UI can be redesigned. A bad API, once published and consumed by external clients, is nearly impossible to change without breaking someone. Every API mistake becomes a permanent tax on your engineering velocity.
The tax is real: One client spent $380K over 18 months maintaining a parallel v2 API because their v1 design was so broken they couldn't incrementally fix it. Three engineers, full-time, just to support both versions while they migrated 200+ external integrations one by one.
Here are the mistakes we find in every audit.
Mistake 1: Inconsistent Naming
What we find:
GET /api/getUsers
GET /api/products/list
POST /api/create-order
PUT /api/users/updateProfile
DELETE /api/removeItem/123
Five endpoints, five different naming conventions.
What it should be:
GET /api/users
GET /api/products
POST /api/orders
PUT /api/users/:id/profile
DELETE /api/items/:id
Rules:
- Nouns, not verbs (HTTP method IS the verb)
- Plural resource names (users, not user)
- Kebab-case for multi-word resources (order-items)
- Consistent nesting (max 2 levels deep)
Mistake 2: No Versioning Strategy
Day 1: GET /api/users returns { name: "John" }
Day 90: Marketing wants full name split
Day 91: GET /api/users returns { firstName: "John", lastName: "Doe" }
Day 92: Every mobile app in production breaks
Day 93: Support tickets flood in, App Store rating drops from 4.7 to 3.9
Day 94: Emergency rollback, 16 engineering hours burned
Week 2: CTO mandate: "No API changes without major version bump"
Month 1: Still supporting both formats, technical debt created
This costs you: One breaking change = 40-80 hours of emergency fixes,
customer support surge, and permanent maintenance burden. We've seen
this cascade cost $45K-120K depending on integration count.
The fix: Version from day one.
Option A: URL versioning (simplest, recommended)
GET /api/v1/users
GET /api/v2/users
Option B: Header versioning
GET /api/users
Accept: application/vnd.myapp.v2+json
Option C: Query parameter
GET /api/users?version=2
Pick one. URL versioning is the most visible and debuggable.
Mistake 3: Leaking Internal Data
// BAD: Returning the database row directly
app.get("/api/users/:id", async (req, res) => {
const user = await db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
res.json(user); // Exposes password_hash, internal_notes, stripe_customer_id
});
// GOOD: Explicit response shaping
app.get("/api/users/:id", async (req, res) => {
const user = await db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
res.json({
id: user.id,
name: user.name,
email: user.email,
createdAt: user.created_at,
// password_hash, internal_notes, stripe_id — intentionally omitted
});
});Rule: Never return database rows directly. Always map to an explicit response shape. This prevents accidentally leaking sensitive data when new columns are added.
Mistake 4: No Pagination
GET /api/products → returns all 50,000 products
This works with 100 products. At 50,000, it:
- Times out the request (30s timeout → 502 Bad Gateway)
- Consumes 200MB of memory per request
- Takes 8 seconds to serialize
- Crashes the client trying to parse it
- Kills your database connection pool (each request holds connection for 8s)
We've seen this fail: One API endpoint without pagination brought down
an entire platform during a product catalog sync. 500 concurrent requests
each holding a DB connection for 8+ seconds = pool exhaustion. Site down
for 23 minutes. Revenue lost during Black Friday: $127K.
Why this costs you: Without pagination, every endpoint is a potential DoS
vector. The fix takes 2 hours. The incident costs 100x that.
Standard pagination:
GET /api/products?page=1&limit=20
Response:
{
"data": [...20 items...],
"pagination": {
"page": 1,
"limit": 20,
"total": 50000,
"totalPages": 2500,
"hasMore": true
}
}
Cursor pagination (better for large datasets):
GET /api/products?limit=20&after=cursor_abc123
Response:
{
"data": [...20 items...],
"pagination": {
"limit": 20,
"nextCursor": "cursor_def456",
"hasMore": true
}
}
Mistake 5: Ignoring Error Responses
What we see:
Error: { "error": "Something went wrong" }
Error: { "message": "Bad request" }
Error: { "success": false }
Error: 500 Internal Server Error (empty body)
What it should be (consistent error format):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"received": "not-an-email"
}
],
"requestId": "req_abc123"
}
}
HTTP status codes used correctly:
400: Client sent bad data (validation errors)
401: Not authenticated (no valid token)
403: Authenticated but not authorized (wrong permissions)
404: Resource doesn't exist
409: Conflict (duplicate entry, version mismatch)
422: Unprocessable entity (valid format, invalid business logic)
429: Rate limited
500: Server error (our fault, not yours)
Mistake 6: Chatty APIs
The mobile app needs to render a product page.
With a chatty API:
GET /api/products/123 → Product details (200ms)
GET /api/products/123/images → Product images (200ms)
GET /api/products/123/reviews → Reviews (200ms)
GET /api/products/123/related → Related products (200ms)
GET /api/inventory/123 → Stock status (200ms)
GET /api/pricing/123 → Current price (200ms)
6 round trips × 200ms latency = 1,200ms just waiting for network,
before any rendering. Add processing time: 1,800ms to interactive.
Why this costs you: Every 100ms of delay costs you 1% of conversions
(Amazon research). At 1,800ms, you're losing 7-10% of potential buyers
before they even see the product. For a $2M/month mobile business,
that's $140K-200K in lost revenue annually. The fix (include parameter)
takes one day to implement.
Better: Include related data via query parameters
GET /api/products/123?include=images,reviews,related,inventory,pricing
1 round trip. Server joins the data. Response in 200ms.
Mistake 7: No Rate Limiting
Without rate limiting:
- A single misbehaving client can DoS your API
- Scraping bots consume your compute budget
- Credential stuffing attacks go unchecked
Real failure: A client's API had no rate limits. A partner integration
had a bug that retried failed requests with no backoff. One bad deploy
on their side = 50,000 requests/second to the API. Site down for 94
minutes. AWS bill spiked from $4K to $31K that month. Three enterprise
customers churned because their integrations were down. Cost: $280K in
lost contracts plus the AWS overage.
Implement rate limiting at multiple levels:
Global: 1,000 requests per minute per IP
Per-user: 100 requests per minute per API key
Per-endpoint: Sensitive endpoints get tighter limits
POST /api/auth/login → 5 per minute per IP
POST /api/orders → 10 per minute per user
GET /api/products → 100 per minute per user
Return rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1706745600
When limited, return:
429 Too Many Requests
Retry-After: 30
Mistake 8: Breaking Changes Without Warning
API lifecycle:
1. Design → Document → Build → Test → Release
2. Monitor usage → Deprecation notice (90 days minimum)
3. New version available → Migration guide published
4. Old version sunset → Only after confirming no active users
Deprecation headers:
Deprecation: true
Sunset: Sat, 01 Jun 2026 00:00:00 GMT
Link: <https://api.example.com/docs/migration-v3>; rel="successor-version"
The API Design Checklist
Before releasing any API endpoint:
□ Consistent naming (nouns, plural, kebab-case)
□ Versioned URL (/api/v1/...)
□ Request validation with clear error messages
□ Response shaping (no raw database rows)
□ Pagination for list endpoints
□ Rate limiting configured
□ Authentication/authorization checked
□ Error format follows standard schema
□ OpenAPI/Swagger documentation generated
□ At least one integration test
□ Monitoring and alerting configured
The Cost of Getting It Wrong
API mistake impact (real client data):
Breaking change without versioning:
→ 40-80 engineering hours fixing integrations
→ 200-500 support tickets
→ App Store rating drop
→ Cost: $45K-120K per incident
No pagination:
→ Production outage during traffic spike
→ 20-90 minutes downtime
→ Cost: $50K-200K in lost revenue (depends on GMV)
Chatty API design:
→ 7-10% conversion rate loss on mobile
→ Cost: $100K-400K annually for mid-market e-commerce
No rate limiting:
→ Single bad client takes down entire platform
→ AWS bill spike
→ Customer churn
→ Cost: $150K-500K per major incident
The pattern: Every mistake takes 2-8 hours to fix correctly but costs
30-100x that when fixed reactively. Design right from day one.
Every API decision you make today becomes a constraint you live with tomorrow. The cost of fixing these mistakes after launch is 10-100x the cost of getting them right the first time. Design your APIs as if they'll be consumed by developers you'll never meet — because they will be.