Skip to main content

REST API Design - Building Endpoints That Work

By · · 5 min read

REST API Design

REST API Design Best Practices: Build APIs Like a Pro

Introduction

I built my first REST API without thinking about design. Endpoints everywhere. Weird naming. Inconsistent responses. When I came back to it 6 months later, I had no idea what I built.

Then I built a second API actually thinking about the structure. Consistent naming. Logical endpoints. Error handling. Using the right HTTP methods. It felt so much better.

Later when my team actually used these APIs, the poorly designed one drove them crazy. The well-designed one? They loved it. Could figure it out without documentation.

That's when I understood: API design matters. A lot. This guide covers how to build APIs that actually work and that people enjoy using.


REST Fundamentals

What is REST?

REST (Representational State Transfer) is an architectural style using HTTP to perform CRUD (Create, Read, Update, Delete) operations on resources:

  • Stateless: Each request contains all needed information
  • Resource-oriented: URLs represent resources, not actions
  • HTTP methods: Use appropriate verbs (GET, POST, PUT, DELETE)
  • Status codes: Use standard HTTP codes for responses

URL Naming Conventions

Resources and Collections

# Collection endpoints
GET    /api/users               # Get all users
POST   /api/users               # Create new user

# Single resource endpoints
GET    /api/users/123           # Get specific user
PUT    /api/users/123           # Update specific user
DELETE /api/users/123           # Delete specific user

# Nested resources
GET    /api/users/123/posts     # Get posts by user 123
POST   /api/users/123/posts     # Create post for user 123
GET    /api/users/123/posts/45  # Get specific post

URL Best Practices

✓ Use nouns for resources (not verbs)
  GET /users              ✓
  GET /getUsers           ✗

✓ Use lowercase and hyphens
  /user-profiles/123      ✓
  /UserProfiles/123       ✗

✓ Use plurals for collections
  GET /products           ✓
  GET /product            ✗

✓ Use IDs in URLs
  /users/123              ✓
  /users/john             ✗ (use IDs, not names)

✓ Keep URLs simple (1-2 levels deep)
  /users/123/posts/45/comments/67  ✗ Too nested

HTTP Methods: Use Them Correctly

GET - Retrieve Data

GET /api/users              # Fetch all users
GET /api/users/123          # Fetch specific user
GET /api/users?status=active&limit=10   # Filter and paginate

✓ Safe: Doesn't modify data
✓ Idempotent: Multiple calls return same result
✓ Cacheable: Response can be cached

POST - Create Data

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

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

✓ Used for creation
✓ NOT idempotent: Multiple calls create multiple resources
✗ NOT safe: Modifies data

Response: 201 Created with Location header

PUT - Replace Entire Resource

PUT /api/users/123
{
  "name": "Jane Doe",
  "email": "jane@example.com"
}

✓ Replaces entire resource
✓ Idempotent: Multiple calls have same effect
✓ Must include all required fields

PATCH - Partial Update

PATCH /api/users/123
{
  "email": "newemail@example.com"
}

✓ Updates specific fields only
✓ More efficient than PUT
✓ Partial resource allowed

DELETE - Remove Resource

DELETE /api/users/123

✓ Removes resource
✓ Idempotent: Multiple calls same result
✓ Response: 204 No Content

Method Cheatsheet

Method CRUD Idempotent Safe
GET Read Yes Yes
POST Create No No
PUT Update Yes No
PATCH Partial Update No No
DELETE Delete Yes No

HTTP Status Codes

Success Responses (2xx)

200 OK              - Request succeeded, data returned
201 Created         - Resource created successfully
204 No Content      - Request succeeded, no content to return

Redirect Responses (3xx)

301 Moved Permanently   - Resource moved, update URL
302 Found              - Temporary redirect
304 Not Modified       - Cache is still valid

Client Error Responses (4xx)

400 Bad Request         - Invalid request format
401 Unauthorized        - Authentication required
403 Forbidden          - Authenticated but no permission
404 Not Found          - Resource doesn't exist
409 Conflict           - Request conflicts (duplicate email)
422 Unprocessable Entity - Validation failed
429 Too Many Requests  - Rate limited

Server Error Responses (5xx)

500 Internal Server Error   - Server error
502 Bad Gateway            - Upstream error
503 Service Unavailable    - Server overloaded

Request and Response Format

Standard JSON Response

{
  "success": true,
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com",
    "createdAt": "2025-11-25T10:30:00Z"
  },
  "message": "User created successfully"
}

Error Response

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      {
        "field": "email",
        "message": "Email already exists"
      },
      {
        "field": "age",
        "message": "Age must be 18+"
      }
    ]
  }
}

Pagination

{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 500,
    "pages": 25,
    "hasNext": true,
    "hasPrev": false
  }
}

Advanced API Design Patterns

API Versioning

# URL versioning (explicit, easy for clients)
GET /api/v1/users
GET /api/v2/users

# Header versioning (cleaner URLs)
GET /api/users
Accept: application/vnd.api+json;version=2

# Parameter versioning
GET /api/users?version=2

# Best practice: Use URL versioning for major changes

Filtering, Sorting, Pagination

# Filtering
GET /api/users?role=admin&status=active

# Sorting
GET /api/users?sort=-createdAt,name

# Pagination
GET /api/users?page=2&limit=50

# Combined
GET /api/users?role=admin&sort=-createdAt&page=1&limit=20

Rate Limiting Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640000000

# Client knows: 1000 requests/hour, 999 remaining

Security Best Practices

Authentication

# Use Bearer tokens (JWT)
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

# Never send credentials in URL
❌ GET /api/users?token=abc123
✓ Authorization: Bearer abc123

HTTPS Only

✓ Always use HTTPS
✓ Set HSTS header: Strict-Transport-Security: max-age=31536000

Input Validation

app.post('/api/users', (req, res) => {
  const { name, email, age } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ 
      error: "Name and email required" 
    });
  }
  
  if (age < 18) {
    return res.status(400).json({ 
      error: "Must be 18+" 
    });
  }
  
  // Process valid data
});

CORS Policy

// Allow specific origins
app.use(cors({
  origin: ['https://frontend.com'],
  credentials: true
}));

Real-World API Example

// Express.js REST API example
const express = require('express');
const app = express();

app.use(express.json());

// GET all users with pagination
app.get('/api/users', (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  const offset = (page - 1) * limit;
  
  // Fetch from DB
  const users = getUsersFromDB(offset, limit);
  
  res.json({
    success: true,
    data: users,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total: getTotalUserCount()
    }
  });
});

// POST create user
app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({
      success: false,
      error: { message: "Name and email required" }
    });
  }
  
  const user = createUserInDB({ name, email });
  
  res.status(201)
    .set('Location', `/api/users/${user.id}`)
    .json({
      success: true,
      data: user,
      message: "User created"
    });
});

// GET specific user
app.get('/api/users/:id', (req, res) => {
  const user = getUserFromDB(req.params.id);
  
  if (!user) {
    return res.status(404).json({
      success: false,
      error: { message: "User not found" }
    });
  }
  
  res.json({ success: true, data: user });
});

// PATCH update user
app.patch('/api/users/:id', (req, res) => {
  const user = updateUserInDB(req.params.id, req.body);
  res.json({ success: true, data: user });
});

// DELETE user
app.delete('/api/users/:id', (req, res) => {
  deleteUserFromDB(req.params.id);
  res.status(204).send();
});

app.listen(3000);

API Documentation

Use tools like Swagger/OpenAPI:

/api/users:
  get:
    summary: List all users
    parameters:
      - name: page
        in: query
        type: integer
    responses:
      200:
        description: List of users
        schema:
          $ref: '#/definitions/UserList'

Tools and Testing

  • Postman: API testing and documentation
  • Insomnia: Lightweight REST client
  • cURL: Command-line testing
  • API docs: Swagger UI, ReDoc
# cURL examples
curl -X GET https://localhost:3000/api/users
curl -X POST https://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John","email":"john@example.com"}'

Checklist for Professional APIs

  • ✓ Use proper HTTP methods for operations
  • ✓ Return appropriate status codes
  • ✓ Implement pagination for collections
  • ✓ Version your API from day one
  • ✓ Use consistent JSON response format
  • ✓ Implement proper error handling
  • ✓ Require authentication for protected routes
  • ✓ Document your API thoroughly
  • ✓ Implement rate limiting
  • ✓ Use HTTPS for all endpoints
  • ✓ Test edge cases and errors

Conclusion

Good REST API design isn't fancy—it's following proven conventions so developers intuitively understand your API. These patterns scale from startups to enterprise, making your APIs professional, maintainable, and a pleasure to use.

Design APIs today that developers will love tomorrow! 🚀