REST API Design Best Practices: Building Developer-Friendly APIs

REST API Design Best Practices: Building Developer-Friendly APIs

BySanjay Goraniya
3 min read
Share:

REST API Design Best Practices: Building Developer-Friendly APIs

A well-designed REST API is a joy to use. A poorly designed one is a constant source of frustration. After designing and consuming dozens of APIs, I've learned what makes an API great—and what makes it terrible.

REST Principles

What is REST?

REST (Representational State Transfer) is an architectural style for designing networked applications. It's based on a few key principles:

  • Stateless - Each request contains all information needed
  • Resource-based - URLs represent resources, not actions
  • HTTP methods - Use standard HTTP verbs (GET, POST, PUT, DELETE)
  • Standard status codes - Use appropriate HTTP status codes

URL Design

Use Nouns, Not Verbs

Code
# Bad: Verbs in URL
GET /api/getUser/123
POST /api/createUser
PUT /api/updateUser/123
DELETE /api/deleteUser/123

# Good: Nouns in URL
GET /api/users/123
POST /api/users
PUT /api/users/123
DELETE /api/users/123

Use Plural Nouns

Code
# Consistent: Use plural
GET /api/users
GET /api/orders
GET /api/products

# Not: Mix of singular and plural
GET /api/user
GET /api/orders
GET /api/product

Use Hierarchical Structure

Code
# Good: Hierarchical
GET /api/users/123/orders
GET /api/users/123/orders/456/items

# Bad: Flat
GET /api/userOrders?userId=123
GET /api/orderItems?orderId=456

Keep URLs Simple

Code
# Good: Simple and clear
GET /api/users/123
GET /api/orders?status=pending

# Bad: Complex and unclear
GET /api/v1/user-management/users/123
GET /api/orders/filter?filterType=status&filterValue=pending

HTTP Methods

GET - Read

Code
GET /api/users/123
GET /api/users?page=1&limit=10

Characteristics:

  • Idempotent (safe to call multiple times)
  • No side effects
  • Should not modify data

POST - Create

Code
POST /api/users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com"
}

Characteristics:

  • Creates new resources
  • Not idempotent (calling twice creates two resources)
  • Returns 201 Created on success

PUT - Update (Replace)

Code
PUT /api/users/123
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com"
}

Characteristics:

  • Replaces entire resource
  • Idempotent (calling twice has same effect)
  • Returns 200 OK or 204 No Content

PATCH - Update (Partial)

Code
PATCH /api/users/123
Content-Type: application/json

{
  "email": "newemail@example.com"
}

Characteristics:

  • Updates part of resource
  • Not necessarily idempotent
  • Returns 200 OK or 204 No Content

DELETE - Remove

Code
DELETE /api/users/123

Characteristics:

  • Removes resource
  • Idempotent (deleting twice is same as once)
  • Returns 200 OK or 204 No Content

Status Codes

Success Codes

Code
200 OK          # Successful GET, PUT, PATCH
201 Created     # Successful POST (resource created)
204 No Content  # Successful DELETE or PUT/PATCH with no body

Client Error Codes

Code
400 Bad Request      # Invalid request (malformed JSON, missing fields)
401 Unauthorized     # Not authenticated
403 Forbidden        # Authenticated but not authorized
404 Not Found        # Resource doesn't exist
409 Conflict         # Resource conflict (duplicate, constraint violation)
422 Unprocessable   # Valid format but semantic errors

Server Error Codes

Code
500 Internal Server Error  # Generic server error
502 Bad Gateway            # Upstream server error
503 Service Unavailable    # Service temporarily unavailable

Request and Response Format

Use JSON

Code
// Request
POST /api/users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com"
}

// Response
{
  "id": "123",
  "name": "John Doe",
  "email": "john@example.com",
  "createdAt": "2023-07-22T10:00:00Z"
}

Consistent Structure

Code
// Good: Consistent structure
{
  "data": {
    "id": "123",
    "name": "John Doe"
  }
}

// Or for lists
{
  "data": [
    { "id": "123", "name": "John Doe" },
    { "id": "456", "name": "Jane Smith" }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 100
  }
}

Error Handling

Consistent Error Format

Code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      {
        "field": "email",
        "message": "Email is required"
      }
    ]
  }
}

Use Appropriate Status Codes

Code
// Validation error
if (!email) {
  return res.status(400).json({
    error: {
      code: "VALIDATION_ERROR",
      message: "Email is required"
    }
  });
}

// Not found
const user = await getUser(id);
if (!user) {
  return res.status(404).json({
    error: {
      code: "NOT_FOUND",
      message: "User not found"
    }
  });
}

Versioning

URL Versioning

Code
GET /api/v1/users
GET /api/v2/users

Pros: Clear, explicit Cons: URLs get longer

Header Versioning

Code
GET /api/users
Accept: application/vnd.api+json;version=2

Pros: Clean URLs Cons: Less discoverable

Recommendation

Use URL versioning for public APIs, header versioning for internal APIs.

Pagination

Offset-Based

Code
GET /api/users?page=1&limit=10

Response:

Code
{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 100,
    "totalPages": 10
  }
}

Cursor-Based

Code
GET /api/users?cursor=abc123&limit=10

Response:

Code
{
  "data": [...],
  "pagination": {
    "cursor": "xyz789",
    "hasMore": true
  }
}

Use cursor-based for:

  • Large datasets
  • Real-time data
  • Avoiding offset performance issues

Filtering and Sorting

Filtering

Code
GET /api/users?status=active&role=admin
GET /api/users?createdAfter=2023-01-01

Sorting

Code
GET /api/users?sort=name&order=asc
GET /api/users?sort=-createdAt  # - for descending

Rate Limiting

Headers

Code
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1690123456

Response on Limit

Code
HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded",
    "retryAfter": 60
  }
}

Documentation

OpenAPI/Swagger

Code
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Success

Interactive Documentation

  • Swagger UI
  • Postman Collections
  • API Explorer

Security

Authentication

Code
Authorization: Bearer <token>

HTTPS

Always use HTTPS in production.

Input Validation

Code
// Validate all input
const schema = {
  email: Joi.string().email().required(),
  name: Joi.string().min(1).max(100).required()
};

const { error, value } = schema.validate(req.body);
if (error) {
  return res.status(400).json({ error: error.details });
}

Real-World Example

API: User management API

Endpoints:

Code
# List users
GET /api/v1/users?page=1&limit=10&status=active

# Get user
GET /api/v1/users/123

# Create user
POST /api/v1/users
{
  "name": "John Doe",
  "email": "john@example.com"
}

# Update user
PUT /api/v1/users/123
{
  "name": "John Smith",
  "email": "john@example.com"
}

# Delete user
DELETE /api/v1/users/123

# Get user orders
GET /api/v1/users/123/orders

Error Response:

Code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      {
        "field": "email",
        "message": "Email is required"
      }
    ]
  }
}

Best Practices Summary

  1. Use nouns in URLs - Resources, not actions
  2. Use HTTP methods correctly - GET, POST, PUT, DELETE
  3. Use appropriate status codes - 200, 201, 400, 404, etc.
  4. Consistent error format - Same structure everywhere
  5. Version your API - Plan for changes
  6. Document everything - OpenAPI/Swagger
  7. Validate input - Never trust user input
  8. Use HTTPS - Always in production
  9. Implement rate limiting - Protect your API
  10. Keep it simple - Simple is better than complex

Conclusion

Good API design is about consistency, clarity, and developer experience. Follow these practices, and your APIs will be:

  • Easy to use - Intuitive and predictable
  • Easy to maintain - Consistent patterns
  • Reliable - Proper error handling
  • Secure - Authentication and validation

Remember: Your API is a contract. Make it clear, consistent, and easy to understand.

What API design challenges have you faced? What practices have worked best for your APIs?

Share: