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
# 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
# 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
# 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
# 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
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
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)
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)
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
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
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
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
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
// 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
// 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
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{
"field": "email",
"message": "Email is required"
}
]
}
}
Use Appropriate Status Codes
// 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
GET /api/v1/users
GET /api/v2/users
Pros: Clear, explicit Cons: URLs get longer
Header Versioning
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
GET /api/users?page=1&limit=10
Response:
{
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"totalPages": 10
}
}
Cursor-Based
GET /api/users?cursor=abc123&limit=10
Response:
{
"data": [...],
"pagination": {
"cursor": "xyz789",
"hasMore": true
}
}
Use cursor-based for:
- Large datasets
- Real-time data
- Avoiding offset performance issues
Filtering and Sorting
Filtering
GET /api/users?status=active&role=admin
GET /api/users?createdAfter=2023-01-01
Sorting
GET /api/users?sort=name&order=asc
GET /api/users?sort=-createdAt # - for descending
Rate Limiting
Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1690123456
Response on Limit
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded",
"retryAfter": 60
}
}
Documentation
OpenAPI/Swagger
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
Authorization: Bearer <token>
HTTPS
Always use HTTPS in production.
Input Validation
// 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:
# 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:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{
"field": "email",
"message": "Email is required"
}
]
}
}
Best Practices Summary
- Use nouns in URLs - Resources, not actions
- Use HTTP methods correctly - GET, POST, PUT, DELETE
- Use appropriate status codes - 200, 201, 400, 404, etc.
- Consistent error format - Same structure everywhere
- Version your API - Plan for changes
- Document everything - OpenAPI/Swagger
- Validate input - Never trust user input
- Use HTTPS - Always in production
- Implement rate limiting - Protect your API
- 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?
Related Posts
API Design Principles: Creating Developer-Friendly REST APIs
Learn the principles and patterns for designing REST APIs that developers love to use. From URL structure to error handling, this guide covers it all.
GraphQL vs REST: Making the Right API Choice in 2025
A comprehensive comparison of GraphQL and REST APIs in 2025. Learn when to use each approach, their trade-offs, and how to make the right decision for your project.
Building Accessible Web Applications: A Developer's Guide
Learn how to build web applications that are accessible to everyone. From semantic HTML to ARIA attributes, master the techniques that make the web inclusive.
Security Best Practices for Full-Stack Applications
Essential security practices every full-stack developer should know. From authentication to data protection, learn how to build secure applications.
Serverless Architecture: When to Use and When to Avoid
A practical guide to serverless architecture. Learn when serverless makes sense, its trade-offs, and how to build effective serverless applications.
Modern Authentication: OAuth 2.0, JWT, and Session Management
Master modern authentication patterns including OAuth 2.0, JWT tokens, and session management. Learn when to use each approach and how to implement them securely.