API Design Best Practices: Building Interfaces That Last

After designing APIs for systems serving millions of requests per day, I’ve learned that good API design is like good architecture—it should be intuitive, scalable, and stand the test of time.

Here are the principles I follow when designing APIs that developers actually want to use.

1. Design for Your Users, Not Your Database

Bad API Design (database-driven):

GET /api/user_profiles?user_id=123
{
  "user_profile_id": 456,
  "user_id": 123,
  "first_name": "John",
  "last_name": "Doe",
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-02-20T09:15:00Z"
}

Good API Design (user-driven):

GET /api/users/123/profile
{
  "id": 123,
  "name": "John Doe",
  "joinedAt": "2024-01-15T10:30:00Z"
}

The second version focuses on what the client needs, not how you store the data internally.

2. Consistency is King

Choose conventions and stick to them religiously:

URL Patterns

# Resources (nouns, not verbs)
GET    /api/users           # List users
POST   /api/users           # Create user  
GET    /api/users/123       # Get specific user
PUT    /api/users/123       # Update user
DELETE /api/users/123       # Delete user

# Nested resources
GET    /api/users/123/posts # User's posts
POST   /api/users/123/posts # Create post for user

Response Formats

{
  "data": { ... },           // Always wrap data
  "meta": {                  // Consistent metadata
    "timestamp": "2024-02-20T09:15:00Z",
    "version": "v1"
  },
  "errors": []               // Even when empty
}

3. Version from Day One

I learned this the hard way. Always version your APIs, even v1:

# URL versioning (simple and visible)
GET /api/v1/users/123

# Header versioning (cleaner URLs)
GET /api/users/123
Accept: application/vnd.yourapi.v1+json

4. Error Handling That Actually Helps

Generic error messages are useless. Be specific and actionable:

// Bad 
{
  "error": "Invalid request"
}

// Good 
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid email format",
    "details": {
      "field": "email",
      "value": "not-an-email",
      "expected": "Valid email address (e.g., [email protected])"
    },
    "documentation": "https://docs.yourapi.com/errors#validation-error"
  }
}

5. HTTP Status Codes Matter

Use them correctly and consistently:

// Success
200 OK          // General success
201 Created     // Resource created
204 No Content  // Success, but no data to return

// Client Errors
400 Bad Request     // Invalid request format
401 Unauthorized    // Authentication required
403 Forbidden       // Authenticated but not allowed
404 Not Found       // Resource doesn't exist
409 Conflict        // Resource already exists
422 Unprocessable   // Valid format, invalid data

// Server Errors  
500 Internal Error  // Our fault
503 Service Unavailable // Temporarily down

6. Pagination for Performance

Never return unlimited results:

GET /api/users?page=1&limit=20

{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1543,
    "totalPages": 78,
    "hasNext": true,
    "hasPrevious": false,
    "nextPage": "/api/users?page=2&limit=20",
    "previousPage": null
  }
}

7. Security by Design

Authentication

# JWT in headers (not URLs!)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Rate Limiting

// Include rate limit info in responses
{
  "data": { ... },
  "rateLimit": {
    "limit": 1000,
    "remaining": 999,
    "resetTime": "2024-02-20T10:00:00Z"
  }
}

Input Validation

// Validate everything, trust nothing
const userSchema = {
  email: { type: 'email', required: true },
  age: { type: 'integer', min: 13, max: 120 },
  name: { type: 'string', minLength: 2, maxLength: 50 }
};

8. Documentation as Code

Use tools like OpenAPI/Swagger to keep docs in sync:

# openapi.yml
paths:
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        200:
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

9. Testing Your API

Write tests that think like your users:

describe('User API', () => {
  test('should create user with valid data', async () => {
    const userData = { name: 'John', email: '[email protected]' };
    const response = await api.post('/users', userData);
    
    expect(response.status).toBe(201);
    expect(response.data.id).toBeDefined();
    expect(response.data.name).toBe('John');
  });
  
  test('should reject invalid email', async () => {
    const userData = { name: 'John', email: 'not-an-email' };
    const response = await api.post('/users', userData);
    
    expect(response.status).toBe(400);
    expect(response.data.error.field).toBe('email');
  });
});

10. Performance Considerations

Field Selection

# Let clients choose what they need
GET /api/users/123?fields=id,name,email

Caching Headers

# Cache static data aggressively
Cache-Control: public, max-age=3600

# Cache dynamic data carefully  
Cache-Control: private, max-age=300

Async for Heavy Operations

// For operations that take time
POST /api/users/123/export
{
  "jobId": "abc123",
  "status": "processing",
  "estimatedCompletion": "2024-02-20T09:20:00Z",
  "statusUrl": "/api/jobs/abc123"
}

Common Mistakes to Avoid

  1. Exposing internal IDs → Use UUIDs or encode them
  2. Ignoring HTTP methods → POST for everything is wrong
  3. No input validation → Garbage in, security issues out
  4. Chatty APIs → One request shouldn’t require 10 more
  5. No backwards compatibility → Breaking changes break trust

GraphQL vs REST: When to Choose What

Use REST when:

  • Simple, well-defined resources
  • Caching is important
  • Team is familiar with REST patterns

Use GraphQL when:

  • Clients need different data shapes
  • Multiple data sources to aggregate
  • Strong typing is valuable

Conclusion

Great API design is about empathy—understanding how developers will use your interface and making their lives easier. It’s not about showing off your technical skills; it’s about creating tools that solve real problems elegantly.

Remember: APIs are forever. The extra time you spend designing them well upfront will save you (and your users) countless hours later.


Have you encountered APIs that made you want to quit programming? Or ones that were so well-designed they made your day? I’d love to hear your stories.