REST API Development

API Design Principles & Best Practices

20 min Lesson 3 of 35

Why API Design Matters

A well-designed API is like a well-organized library. Users can find what they need quickly, understand how to use it intuitively, and feel confident that it won't change unexpectedly. Poor API design leads to confusion, frustration, and maintenance nightmares.

Note: You only get one chance to design your API right. Once it's public and people depend on it, making breaking changes becomes extremely difficult. Invest time in good design upfront.

URL Structure: The Foundation

URLs are the entry points to your API. They should be intuitive, consistent, and predictable. Think of URLs as the table of contents for your API.

Core Principles:

1. Use Nouns, Not Verbs

URLs should represent resources (nouns), not actions (verbs). The HTTP method indicates the action.

❌ Bad - Verbs in URLs:
GET  /api/getUsers
POST /api/createUser
POST /api/updateUser/123
POST /api/deleteUser/123

✅ Good - Nouns with HTTP methods:
GET    /api/users          // Get all users
POST   /api/users          // Create user
PUT    /api/users/123      // Update user
DELETE /api/users/123      // Delete user

2. Use Plural Nouns for Collections

Collections should always use plural nouns for consistency, even when returning a single item.

❌ Bad - Inconsistent singular/plural:
GET /api/user/123        // Single user
GET /api/users           // Multiple users
GET /api/product/456     // Single product
GET /api/products        // Multiple products

✅ Good - Always plural:
GET /api/users/123       // Single user
GET /api/users           // Multiple users
GET /api/products/456    // Single product
GET /api/products        // Multiple products
Tip: Stick to plural nouns even if it feels grammatically awkward. Consistency is more important than perfect grammar in API design.

3. Hierarchical Structure for Relationships

Use nested URLs to represent relationships between resources.

// User's posts
GET /api/users/123/posts

// Specific post by a user
GET /api/users/123/posts/456

// Comments on a post
GET /api/posts/456/comments

// Specific comment
GET /api/posts/456/comments/789

// Nested three levels (generally avoid going deeper)
GET /api/users/123/posts/456/comments
Warning: Avoid nesting more than 2-3 levels deep. Deep nesting creates long, complex URLs that are hard to understand and maintain. Use query parameters or separate endpoints instead.

4. Use Lowercase and Hyphens

URLs should be lowercase with hyphens separating words, not underscores or camelCase.

❌ Bad:
/api/UserProfiles
/api/user_profiles
/api/userProfiles

✅ Good:
/api/user-profiles

5. Don't Use File Extensions

Modern APIs use Content-Type headers, not file extensions.

❌ Bad:
GET /api/users.json
GET /api/users.xml

✅ Good:
GET /api/users
Accept: application/json

Resource Naming Conventions

Consistent naming makes your API predictable and easy to learn.

Standard Resource Patterns:

Pattern Purpose Example
/resources Collection of resources /users, /products, /orders
/resources/:id Specific resource /users/123, /products/abc
/resources/:id/subresources Nested collection /users/123/posts
/resources/actions Non-CRUD operations /users/search, /orders/export

Naming Special Endpoints:

// Search and filtering
GET /api/users/search?q=john
GET /api/products?category=electronics&price_max=1000

// Batch operations
POST /api/users/batch-create
POST /api/products/batch-update
DELETE /api/users/batch-delete

// Actions that don't fit CRUD
POST /api/users/123/activate
POST /api/orders/456/cancel
POST /api/invoices/789/send

// Aggregations and statistics
GET /api/users/count
GET /api/sales/statistics
GET /api/reports/summary

// Authentication and authorization
POST /api/auth/login
POST /api/auth/logout
POST /api/auth/refresh
GET /api/auth/me

API Versioning: Planning for Change

Your API will evolve. Versioning allows you to make changes without breaking existing clients.

Versioning Strategies:

1. URL Path Versioning (Recommended)

Most common and visible. Version is part of the URL path.

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

Pros:
✅ Clear and visible
✅ Easy to route different versions
✅ Simple to implement
✅ Works with all HTTP clients

Cons:
❌ URLs change between versions
❌ Can lead to code duplication

2. Query Parameter Versioning

Version specified as a query parameter.

GET /api/users?version=1
GET /api/users?version=2

Pros:
✅ Base URL stays the same
✅ Easy to default to latest version

Cons:
❌ Less visible
❌ Can be forgotten in requests
❌ Harder to cache

3. Header Versioning

Version specified in custom header or Accept header.

GET /api/users
X-API-Version: 1

or

GET /api/users
Accept: application/vnd.myapi.v1+json

Pros:
✅ Clean URLs
✅ Follows REST principles (content negotiation)

Cons:
❌ Not visible in URLs
❌ Harder to test (need to set headers)
❌ More complex to implement

4. Subdomain Versioning

Version specified as subdomain.

GET https://api-v1.example.com/users
GET https://api-v2.example.com/users

Pros:
✅ Complete isolation between versions
✅ Easy to deploy versions separately

Cons:
❌ Requires DNS management
❌ SSL certificate complexity
❌ More infrastructure overhead
Tip: For most projects, URL path versioning (/api/v1/) is the best choice. It's clear, simple, and widely adopted by major APIs like Twitter, GitHub, and Stripe.

Versioning Best Practices:

  • Start with v1: Don't wait for the "perfect" API. Ship v1 and iterate.
  • Use integer versions: v1, v2, v3 (not v1.1, v1.2)
  • Maintain old versions: Give clients time to migrate (6-12 months)
  • Document deprecation: Clearly communicate when versions will sunset
  • Add deprecation headers: Warn clients about deprecated endpoints
// Deprecation warning in response headers
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"

{
  "data": [...],
  "meta": {
    "deprecation_warning": "This endpoint will be removed on Dec 31, 2026. Migrate to v2."
  }
}

Query Parameters: Filtering, Sorting, and Pagination

Query parameters extend your API's capabilities without cluttering URLs.

Filtering:

// Single filter
GET /api/users?status=active

// Multiple filters
GET /api/users?status=active&role=admin

// Range filters
GET /api/products?price_min=10&price_max=100
GET /api/posts?created_after=2026-01-01

// Partial match
GET /api/users?name_contains=john
GET /api/products?sku_starts_with=ABC

// Multiple values (comma-separated or repeated param)
GET /api/users?roles=admin,editor
GET /api/users?role=admin&role=editor

Sorting:

// Simple sort
GET /api/users?sort=name

// Descending order (prefix with minus)
GET /api/users?sort=-created_at

// Multiple sort fields
GET /api/users?sort=role,-created_at

// Alternative syntax
GET /api/users?sort_by=name&order=asc

Pagination:

Essential for large datasets. Multiple approaches exist:

Offset-Based Pagination:

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

Response:
{
  "data": [...],
  "pagination": {
    "current_page": 1,
    "per_page": 20,
    "total": 150,
    "total_pages": 8,
    "links": {
      "first": "/api/users?page=1",
      "prev": null,
      "next": "/api/users?page=2",
      "last": "/api/users?page=8"
    }
  }
}

Cursor-Based Pagination (Better for Large Datasets):

GET /api/users?cursor=abc123&limit=20

Response:
{
  "data": [...],
  "pagination": {
    "next_cursor": "xyz789",
    "prev_cursor": "abc123",
    "has_more": true
  }
}

Advantages:
✅ Consistent results (no duplicates/skips)
✅ Better performance on large datasets
✅ Works well with real-time data

Field Selection (Sparse Fieldsets):

Let clients request only the fields they need.

// Select specific fields
GET /api/users?fields=id,name,email

// Exclude fields
GET /api/users?fields=-password,-ssn

// Nested fields
GET /api/users?fields=id,name,profile.avatar

Expanding Relationships:

Include related resources in a single request.

// Default response (no expansion)
GET /api/posts/123
{
  "id": 123,
  "title": "My Post",
  "author_id": 456
}

// Expanded relationships
GET /api/posts/123?expand=author,comments
{
  "id": 123,
  "title": "My Post",
  "author": {
    "id": 456,
    "name": "John Doe"
  },
  "comments": [
    {"id": 1, "text": "Great post!"}
  ]
}

Consistency: The Golden Rule

Consistency matters more than perfection. Once you establish patterns, stick to them throughout your API.

Areas to Keep Consistent:

  • Naming conventions: camelCase vs snake_case (pick one)
  • Date formats: ISO 8601 (2026-02-14T10:30:00Z)
  • Error response format: Same structure everywhere
  • Pagination style: Don't mix offset and cursor pagination
  • Boolean values: true/false, not 1/0 or yes/no
  • Null handling: Include null fields or omit them (pick one)
// Consistent response structure
{
  "data": {...},           // Actual resource data
  "meta": {...},           // Metadata (pagination, counts)
  "links": {...}           // Hypermedia links
}

// Consistent error structure
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "message": "Email is required"
      }
    ]
  }
}

Real-World API Examples

Let's look at how leading companies structure their APIs:

GitHub API:

GET /repos/{owner}/{repo}
GET /repos/{owner}/{repo}/issues
GET /repos/{owner}/{repo}/issues/{number}
GET /repos/{owner}/{repo}/pulls
GET /user/repos?sort=updated&direction=desc

Features:
✅ Clear hierarchical structure
✅ URL path versioning (/v3/)
✅ Comprehensive filtering
✅ Cursor pagination for large sets

Stripe API:

GET /v1/customers
POST /v1/customers
GET /v1/customers/{id}
GET /v1/customers/{id}/subscriptions
GET /v1/charges?limit=10

Features:
✅ Simple, predictable URLs
✅ Prefix versioning (/v1/)
✅ Consistent parameter naming
✅ Expandable resources

Twitter API:

GET /2/tweets/{id}
GET /2/users/{id}/tweets
GET /2/tweets/search/recent?query=api

Features:
✅ Version in path (/2/)
✅ Resource-focused design
✅ Query-based search
✅ Rate limiting headers
Exercise: Design Your Own API

Design REST API endpoints for a simple blog system with these requirements:

  • Users can create, read, update, delete blog posts
  • Posts can have multiple comments
  • Posts can be filtered by category and author
  • Comments can be paginated
  • Users can like posts

Design these endpoints:

  1. Get all posts: _________________
  2. Create a new post: _________________
  3. Get a specific post: _________________
  4. Update a post: _________________
  5. Delete a post: _________________
  6. Get comments for a post: _________________
  7. Add a comment to a post: _________________
  8. Like a post: _________________
  9. Get posts by author: _________________
  10. Get posts with pagination: _________________

Sample Answers:

  1. GET /api/v1/posts
  2. POST /api/v1/posts
  3. GET /api/v1/posts/{id}
  4. PUT /api/v1/posts/{id} or PATCH /api/v1/posts/{id}
  5. DELETE /api/v1/posts/{id}
  6. GET /api/v1/posts/{id}/comments
  7. POST /api/v1/posts/{id}/comments
  8. POST /api/v1/posts/{id}/likes
  9. GET /api/v1/posts?author_id=123
  10. GET /api/v1/posts?page=1&limit=20

Key Takeaways

  • Use nouns in URLs, not verbs - let HTTP methods define actions
  • Always use plural nouns for collections for consistency
  • Keep URLs hierarchical but avoid nesting more than 2-3 levels
  • Use lowercase with hyphens in URLs
  • Version your API from day one (URL path versioning recommended)
  • Provide filtering, sorting, and pagination through query parameters
  • Consistency is more important than perfection
  • Study successful APIs (GitHub, Stripe, Twitter) and adopt their patterns
  • Document everything - good design isn't intuitive without documentation
Next Lesson Preview: Now that you understand how to structure your API, we'll explore request and response formats, including JSON structure, content negotiation, and how to handle different data formats effectively.