Skip to main content

API Design Patterns: RESTful Best Practices, Versioning, and Documentation

Master RESTful API design with best practices, versioning strategies, and comprehensive documentation. Learn to build maintainable, scalable APIs that developers love to use.

Table of Contents

Introduction

Designing a well-structured RESTful API is both an art and a science. A poorly designed API can frustrate developers, create maintenance nightmares, and limit your application’s scalability. On the other hand, a thoughtfully designed API becomes a joy to work with, encourages adoption, and can become a competitive advantage for your platform.

REST (Representational State Transfer) has become the de facto standard for web APIs, but simply using HTTP methods doesn’t guarantee a good API design. Understanding RESTful principles, implementing consistent patterns, handling versioning gracefully, and providing excellent documentation are all critical skills for modern backend developers.

This comprehensive guide will teach you everything you need to know about designing production-ready RESTful APIs. You’ll learn resource naming conventions, HTTP method best practices, versioning strategies, error handling patterns, and how to create documentation that makes your API self-explanatory. Whether you’re building your first API or refining an existing one, these patterns and practices will help you create APIs that are maintainable, scalable, and developer-friendly.

By the end of this guide, you’ll understand how to design RESTful APIs that follow industry best practices, handle versioning without breaking existing clients, and provide documentation that reduces integration time and support requests.


RESTful API Fundamentals

REST is an architectural style, not a protocol or standard. Understanding its core principles is essential before diving into implementation details.

What Makes an API RESTful?

A RESTful API adheres to six key constraints:

  1. Client-Server Architecture: Separation of concerns between client and server
  2. Stateless: Each request contains all information needed to process it
  3. Cacheable: Responses should be cacheable when appropriate
  4. Uniform Interface: Consistent way of interacting with resources
  5. Layered System: Architecture can be composed of hierarchical layers
  6. Code on Demand (optional): Server can send executable code to clients

Core Concepts

Resources: Everything in REST is a resource—users, orders, products, or even abstract concepts like “search results.” Resources are identified by URIs (Uniform Resource Identifiers).

Representations: Resources have multiple representations (JSON, XML, HTML). Clients request specific representations using HTTP headers like Accept: application/json.

Stateless Communication: Each request from client to server must contain all information needed to understand and process the request. The server doesn’t store client context between requests.

HTTP Methods: REST uses standard HTTP methods to indicate the action:

  • GET: Retrieve a resource
  • POST: Create a new resource
  • PUT: Replace a resource (idempotent)
  • PATCH: Partially update a resource
  • DELETE: Remove a resource

REST vs Other Approaches

While REST is the most common API style, it’s not always the best choice. For a detailed comparison with GraphQL, see our guide on GraphQL vs REST API. REST excels at resource-based operations, while GraphQL shines when you need flexible querying and reduced over-fetching.


Resource Design and Naming Conventions

Resource Naming Best Practices

Resource names form the foundation of your API’s usability. Follow these conventions:

Use Nouns, Not Verbs

# ✅ Good: Resource-based
GET /api/users
GET /api/users/123
POST /api/users
# ❌ Bad: Action-based
GET /api/getUsers
POST /api/createUser
GET /api/user/123/delete

Use Plural Nouns for Collections

# ✅ Good
GET /api/users
GET /api/orders
GET /api/products
# ❌ Bad
GET /api/user
GET /api/order

Use Hierarchical Structure for Relationships

# ✅ Good: Clear hierarchy
GET /api/users/123/orders
GET /api/users/123/orders/456
GET /api/orders/456/items
# ❌ Bad: Flat structure
GET /api/userOrders?userId=123
GET /api/orderItems?orderId=456

Use Hyphens, Not Underscores

# ✅ Good
GET /api/user-profiles
GET /api/order-items
# ❌ Bad
GET /api/user_profiles
GET /api/orderItems

Resource Identification Patterns

Simple Resources: Direct access to a resource by ID

GET /api/users/123
DELETE /api/users/123

Nested Resources: Accessing related resources through parent

GET /api/users/123/posts
POST /api/users/123/posts
GET /api/users/123/posts/456

Filtered Collections: Using query parameters for filtering

GET /api/users?status=active&role=admin
GET /api/orders?status=pending&limit=10

Resource Design Example

Here’s a well-designed resource structure for an e-commerce API:

// User resources
GET / api / users; // List all users
POST / api / users; // Create new user
GET / api / users / { id }; // Get specific user
PUT / api / users / { id }; // Update entire user
PATCH / api / users / { id }; // Partial update
DELETE / api / users / { id }; // Delete user
// Order resources (nested under users)
GET / api / users / { id } / orders; // Get user's orders
POST / api / users / { id } / orders; // Create order for user
GET / api / users / { id } / orders / { orderId }; // Get specific order
// Standalone order resources
GET / api / orders; // List all orders
GET / api / orders / { id }; // Get specific order
PATCH / api / orders / { id }; // Update order status

HTTP Methods and Status Codes

HTTP Methods: When to Use Each

Understanding when to use each HTTP method is crucial for RESTful design:

GET: Retrieve data (idempotent, safe, cacheable)

GET /api/users/123
GET /api/users?status=active

POST: Create new resources or perform actions (not idempotent)

POST /api/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}

PUT: Replace entire resource (idempotent)

PUT /api/users/123
Content-Type: application/json
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"role": "admin"
}

PATCH: Partial update (idempotent)

PATCH /api/users/123
Content-Type: application/json
{
"email": "newemail@example.com"
}

DELETE: Remove resource (idempotent)

DELETE /api/users/123

HTTP Status Codes: Complete Guide

Status codes communicate the result of an operation. Use them consistently:

2xx Success Codes

// 200 OK: Successful GET, PUT, PATCH
GET /api/users/123200 OK
// 201 Created: Successful POST (include Location header)
POST /api/users → 201 Created
Location: /api/users/456
// 204 No Content: Successful DELETE or PUT with no response body
DELETE /api/users/123204 No Content

4xx Client Error Codes

// 400 Bad Request: Invalid request syntax or parameters
POST /api/users
{ "invalid": "data" } → 400 Bad Request
// 401 Unauthorized: Missing or invalid authentication
GET /api/users/123401 Unauthorized
// 403 Forbidden: Authenticated but not authorized
GET /api/admin/users → 403 Forbidden
// 404 Not Found: Resource doesn't exist
GET /api/users/999404 Not Found
// 409 Conflict: Resource conflict (e.g., duplicate email)
POST /api/users
{ "email": "existing@example.com" } → 409 Conflict
// 422 Unprocessable Entity: Valid syntax but semantic errors
POST /api/users
{ "email": "invalid-email" } → 422 Unprocessable Entity

5xx Server Error Codes

// 500 Internal Server Error: Unexpected server error
GET /api/users → 500 Internal Server Error
// 503 Service Unavailable: Server temporarily unavailable
GET /api/users → 503 Service Unavailable

Status Code Implementation Example

// Express.js example
app.post("/api/users", async (req, res) => {
try {
const { email, name } = req.body;
// Validation
if (!email || !name) {
return res.status(400).json({
error: "Missing required fields",
fields: { email: "required", name: "required" },
});
}
// Check for duplicate
const existing = await User.findOne({ email });
if (existing) {
return res.status(409).json({
error: "User already exists",
email: email,
});
}
// Create user
const user = await User.create({ email, name });
// Return 201 with Location header
res.status(201).location(`/api/users/${user.id}`).json(user);
} catch (error) {
res.status(500).json({
error: "Internal server error",
message: error.message,
});
}
});

Request and Response Patterns

Request Headers

Standard headers for API requests:

GET /api/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
If-None-Match: "abc123"

Common Headers:

  • Accept: Requested response format (application/json, application/xml)
  • Content-Type: Request body format (application/json)
  • Authorization: Authentication token
  • If-None-Match: Conditional request using ETag
  • If-Modified-Since: Conditional request using timestamp

Response Headers

Standard headers for API responses:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "abc123"
Last-Modified: Wed, 20 Jan 2025 10:00:00 GMT
Cache-Control: public, max-age=3600
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Common Headers:

  • Content-Type: Response format
  • ETag: Entity tag for caching
  • Last-Modified: Resource modification timestamp
  • Cache-Control: Caching directives
  • X-Request-ID: Unique request identifier for debugging

Request Body Patterns

Creating Resources:

POST /api/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"role": "user"
}

Updating Resources:

PATCH /api/users/123
Content-Type: application/json
{
"email": "newemail@example.com"
}

Response Body Patterns

Single Resource:

{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"role": "user",
"createdAt": "2025-01-20T10:00:00Z",
"updatedAt": "2025-01-20T10:00:00Z"
}

Collection:

{
"data": [
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 124,
"name": "Jane Smith",
"email": "jane@example.com"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8
}
}

Error Response:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
],
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
}

Consistent Response Wrapper

Consider wrapping all responses in a consistent structure:

// Success response wrapper
type ApiResponse<T> = {
data: T;
meta?: {
timestamp: string;
requestId: string;
};
};
// Error response wrapper
type ApiError = {
error: {
code: string;
message: string;
details?: unknown;
requestId: string;
};
};
// Usage example
const successResponse: ApiResponse<User> = {
data: {
id: 123,
name: "John Doe",
email: "john@example.com",
},
meta: {
timestamp: new Date().toISOString(),
requestId: "550e8400-e29b-41d4-a716-446655440000",
},
};

API Versioning Strategies

Versioning is crucial for maintaining backward compatibility while evolving your API. Choose a strategy that fits your needs.

Versioning Approaches

1. URL Path Versioning (Most Common)

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

Pros: Clear, explicit, easy to route ❌ Cons: URLs change, can clutter codebase

2. Header Versioning

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

Pros: Clean URLs, RESTful ❌ Cons: Less discoverable, requires header knowledge

3. Query Parameter Versioning

GET /api/users?version=2

Pros: Simple, optional ❌ Cons: Can be ignored, less RESTful

4. Domain Versioning

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

Pros: Complete isolation ❌ Cons: Infrastructure complexity

URL path versioning is the most widely adopted approach:

// Express.js routing example
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);
// Version 1 router
const v1Router = express.Router();
v1Router.get("/users", getUsersV1);
v1Router.get("/users/:id", getUserV1);
// Version 2 router
const v2Router = express.Router();
v2Router.get("/users", getUsersV2);
v2Router.get("/users/:id", getUserV2);

Versioning Best Practices

Always Version from the Start

// ✅ Good: Versioned from day one
GET / api / v1 / users;
// ❌ Bad: No versioning, breaking changes later
GET / api / users; // Now you need to add versioning retroactively

Maintain Backward Compatibility

// Version 1: Returns basic user data
GET /api/v1/users/123
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
// Version 2: Adds new fields but maintains compatibility
GET /api/v2/users/123
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"profile": {
"avatar": "https://...",
"bio": "..."
}
}

Deprecate Gracefully

GET /api/v1/users/123
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 20 Jan 2026 00:00:00 GMT
Link: </api/v2/users/123>; rel="successor-version"
Warning: 299 - "API Version 1 is deprecated. Please migrate to Version 2."

Versioning Implementation Example

// Version middleware
const versionMiddleware = (req: Request, res: Response, next: NextFunction) => {
const version = req.path.split("/")[2]; // Extract version from /api/v1/...
if (!version || !version.startsWith("v")) {
return res.status(400).json({
error: "API version required",
message: "Please specify API version in URL (e.g., /api/v1/users)",
});
}
req.apiVersion = version;
next();
};
// Version-aware handler
const getUsers = async (req: Request, res: Response) => {
const { apiVersion } = req;
if (apiVersion === "v1") {
return getUsersV1(req, res);
} else if (apiVersion === "v2") {
return getUsersV2(req, res);
} else {
return res.status(400).json({
error: "Unsupported API version",
supportedVersions: ["v1", "v2"],
});
}
};

Error Handling and Status Codes

Error Response Structure

Consistent error responses help clients handle errors gracefully:

type ErrorResponse = {
error: {
code: string; // Machine-readable error code
message: string; // Human-readable message
details?: unknown; // Additional error details
requestId: string; // Request ID for debugging
timestamp: string; // Error timestamp
};
};

Error Code Categories

Client Errors (4xx):

// Validation errors
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"fields": [
{
"field": "email",
"message": "Invalid email format"
}
]
},
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
}
// Not found errors
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 123 not found",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
}
// Authentication errors
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired token",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
}

Server Errors (5xx):

{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-01-20T10:00:00Z"
}
}

Error Handling Implementation

// Error handling middleware
class ApiError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: unknown,
) {
super(message);
this.name = "ApiError";
}
}
// Error handler middleware
const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction,
) => {
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details,
requestId: req.id,
timestamp: new Date().toISOString(),
},
});
}
// Unexpected errors
console.error("Unexpected error:", err);
res.status(500).json({
error: {
code: "INTERNAL_SERVER_ERROR",
message: "An unexpected error occurred",
requestId: req.id,
timestamp: new Date().toISOString(),
},
});
};
// Usage in route handlers
app.get("/api/v1/users/:id", async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new ApiError(
404,
"RESOURCE_NOT_FOUND",
`User with ID ${req.params.id} not found`,
);
}
res.json({ data: user });
} catch (error) {
next(error);
}
});

Pagination, Filtering, and Sorting

Pagination Patterns

Offset-Based Pagination:

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

Response:

{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": false
}
}

Cursor-Based Pagination (Better for large datasets):

GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20

Response:

{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MTQzfQ",
"limit": 20,
"hasNext": true
}
}

Filtering

GET /api/v1/users?status=active&role=admin&createdAfter=2025-01-01
// Filtering implementation
const getUsers = async (req: Request, res: Response) => {
const { status, role, createdAfter, email } = req.query;
const filter: Record<string, unknown> = {};
if (status) filter.status = status;
if (role) filter.role = role;
if (createdAfter) filter.createdAt = { $gte: new Date(createdAfter) };
if (email) filter.email = { $regex: email, $options: "i" };
const users = await User.find(filter);
res.json({ data: users });
};

Sorting

GET /api/v1/users?sort=name&order=asc
GET /api/v1/users?sort=-createdAt # Descending order
// Sorting implementation
const getUsers = async (req: Request, res: Response) => {
const { sort = "createdAt", order = "desc" } = req.query;
const sortField = order === "asc" ? sort : `-${sort}`;
const users = await User.find().sort(sortField);
res.json({ data: users });
};

Combined Example

GET /api/v1/users?status=active&role=admin&sort=createdAt&order=desc&page=1&limit=20
// Complete implementation with pagination, filtering, and sorting
const getUsers = async (req: Request, res: Response) => {
const {
status,
role,
sort = "createdAt",
order = "desc",
page = 1,
limit = 20,
} = req.query;
// Build filter
const filter: Record<string, unknown> = {};
if (status) filter.status = status;
if (role) filter.role = role;
// Build sort
const sortField = order === "asc" ? sort : `-${sort}`;
// Calculate pagination
const skip = (Number(page) - 1) * Number(limit);
// Execute query
const [users, total] = await Promise.all([
User.find(filter).sort(sortField).skip(skip).limit(Number(limit)),
User.countDocuments(filter),
]);
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total,
totalPages: Math.ceil(total / Number(limit)),
hasNext: skip + users.length < total,
hasPrev: Number(page) > 1,
},
});
};

API Documentation with OpenAPI

Why OpenAPI?

OpenAPI (formerly Swagger) provides a standard way to document REST APIs. Benefits include:

  • Interactive Documentation: Auto-generated docs with try-it-out functionality
  • Code Generation: Generate client SDKs automatically
  • Validation: Validate requests/responses against schema
  • Testing: Generate test cases from documentation

OpenAPI Specification Example

openapi: 3.0.0
info:
title: User API
version: 1.0.0
description: API for managing users
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
pagination:
$ref: "#/components/schemas/Pagination"
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: User created
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
$ref: "#/components/responses/BadRequest"
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
createdAt:
type: string
format: date-time
CreateUserRequest:
type: object
required:
- name
- email
properties:
name:
type: string
email:
type: string
format: email
Pagination:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
responses:
BadRequest:
description: Bad request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

Generating Documentation

Using Swagger UI:

import swaggerUi from "swagger-ui-express";
import swaggerDocument from "./swagger.json";
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));

Using Code-First Approach (tsoa, nestjs-swagger):

import { Get, Route, Tags, Query } from "tsoa";
@Route("users")
@Tags("Users")
export class UsersController {
@Get()
public async getUsers(
@Query() page: number = 1,
@Query() limit: number = 20,
): Promise<UserResponse> {
// Implementation
}
}

Security and Authentication

Authentication Patterns

RESTful APIs commonly use token-based authentication. For comprehensive coverage, see our guide on Authentication and Authorization.

Bearer Token Authentication:

GET /api/v1/users/123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

API Key Authentication:

GET /api/v1/users/123
X-API-Key: your-api-key-here

Security Best Practices

Always Use HTTPS

// Force HTTPS in production
if (process.env.NODE_ENV === "production") {
app.use((req, res, next) => {
if (req.header("x-forwarded-proto") !== "https") {
res.redirect(`https://${req.header("host")}${req.url}`);
} else {
next();
}
});
}

Validate and Sanitize Input

import { body, validationResult } from "express-validator";
app.post(
"/api/v1/users",
[
body("email").isEmail().normalizeEmail(),
body("name").trim().escape().isLength({ min: 1, max: 100 }),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process request
},
);

Rate Limiting

import rateLimit from "express-rate-limit";
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: "Too many requests from this IP",
});
app.use("/api/", apiLimiter);

CORS Configuration

import cors from "cors";
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || ["https://example.com"],
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
}),
);

Performance Optimization

Caching Strategies

ETag-Based Caching:

import crypto from "crypto";
const generateETag = (data: unknown): string => {
const hash = crypto
.createHash("md5")
.update(JSON.stringify(data))
.digest("hex");
return `"${hash}"`;
};
app.get("/api/v1/users/:id", async (req, res) => {
const user = await User.findById(req.params.id);
const etag = generateETag(user);
// Check if client has cached version
if (req.headers["if-none-match"] === etag) {
return res.status(304).end(); // Not Modified
}
res.set("ETag", etag);
res.set("Cache-Control", "public, max-age=3600");
res.json({ data: user });
});

Response Caching Headers:

// Cache for 1 hour
res.set("Cache-Control", "public, max-age=3600");
// Don't cache sensitive data
res.set("Cache-Control", "no-store, no-cache, must-revalidate");
// Cache but revalidate
res.set("Cache-Control", "public, max-age=3600, must-revalidate");

Compression

import compression from "compression";
app.use(
compression({
level: 6,
threshold: 1024, // Only compress responses > 1KB
}),
);

Database Query Optimization

// ✅ Good: Select only needed fields
const users = await User.find().select("name email").limit(20);
// ✅ Good: Use indexes
// Ensure database indexes on frequently queried fields
User.createIndex({ email: 1 });
User.createIndex({ status: 1, createdAt: -1 });
// ❌ Bad: Fetching all fields and all records
const users = await User.find(); // No limit, all fields

API Evolution and Deprecation

Deprecation Strategy

1. Announce Deprecation Early

GET /api/v1/users/123
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 20 Jan 2026 00:00:00 GMT
Link: </api/v2/users/123>; rel="successor-version"
Warning: 299 - "API Version 1 is deprecated. Please migrate to Version 2 by January 20, 2026."

2. Provide Migration Guide

Create comprehensive migration documentation:

# API v1 to v2 Migration Guide
## Breaking Changes
1. **User Response Format**
- v1: `{ "user": {...} }`
- v2: `{ "data": {...} }`
2. **Pagination**
- v1: `?page=1&perPage=20`
- v2: `?page=1&limit=20`
## Migration Steps
1. Update base URL from `/api/v1` to `/api/v2`
2. Update response parsing logic
3. Update pagination parameters
...

3. Maintain Backward Compatibility

Keep deprecated endpoints working during transition period:

// Deprecated endpoint with redirect
app.get("/api/v1/users/:id", (req, res) => {
res
.status(200)
.set("Deprecation", "true")
.set("Sunset", "Sat, 20 Jan 2026 00:00:00 GMT")
.set(
"Link",
"</api/v2/users/" + req.params.id + '>; rel="successor-version"',
)
.json({
// v1 response format for backward compatibility
user: getUserData(req.params.id),
});
});

Version Lifecycle

v1 (Current) → v2 (Beta) → v2 (Stable) → v1 (Deprecated) → v1 (Sunset)
↓ ↓ ↓ ↓ ↓
6 months 3 months 6 months 6 months Removed

Common Anti-Patterns to Avoid

❌ Using Verbs in URLs

# ❌ Bad
POST /api/createUser
GET /api/getUser/123
DELETE /api/deleteUser/123
# ✅ Good
POST /api/users
GET /api/users/123
DELETE /api/users/123

❌ Inconsistent Naming

# ❌ Bad: Mixed naming conventions
GET /api/userProfiles
GET /api/user-profiles
GET /api/user_profiles
# ✅ Good: Consistent naming
GET /api/user-profiles

❌ Returning HTML Errors

# ❌ Bad: HTML error page
HTTP/1.1 404 Not Found
Content-Type: text/html
<html><body>404 Not Found</body></html>
# ✅ Good: JSON error response
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User not found"
}
}

❌ Ignoring HTTP Methods

# ❌ Bad: Using GET for everything
GET /api/users/create?name=John&email=john@example.com
GET /api/users/123/delete
# ✅ Good: Using appropriate HTTP methods
POST /api/users
DELETE /api/users/123

❌ Inconsistent Response Formats

# ❌ Bad: Different formats for similar endpoints
GET /api/users → { "users": [...] }
GET /api/orders → { "data": [...] }
GET /api/products → [{...}, {...}]
# ✅ Good: Consistent format
GET /api/users → { "data": [...] }
GET /api/orders → { "data": [...] }
GET /api/products → { "data": [...] }

Best Practices Summary

Design Principles

  1. Consistency: Use consistent naming, formatting, and patterns throughout
  2. Simplicity: Keep APIs simple and intuitive
  3. Predictability: Follow REST conventions so APIs are predictable
  4. Documentation: Provide comprehensive, up-to-date documentation
  5. Versioning: Plan for versioning from the start
  6. Error Handling: Provide clear, actionable error messages
  7. Security: Implement authentication, authorization, and input validation
  8. Performance: Optimize with caching, compression, and efficient queries

Checklist for New APIs


Conclusion

Designing a well-structured RESTful API requires attention to detail, consistency, and understanding of both REST principles and HTTP standards. By following the patterns and best practices outlined in this guide, you’ll create APIs that are:

  • Maintainable: Consistent patterns make code easier to understand and modify
  • Scalable: Proper versioning and evolution strategies support growth
  • Developer-Friendly: Clear documentation and predictable behavior reduce integration time
  • Secure: Authentication, validation, and security headers protect your API
  • Performant: Caching, compression, and optimized queries ensure fast responses

Remember that API design is iterative. Start with solid foundations—proper resource naming, HTTP methods, and versioning—then refine based on feedback and usage patterns. Good APIs evolve gracefully, maintaining backward compatibility while adding new capabilities.

For related topics, explore our guides on GraphQL vs REST API for architectural comparisons, and Authentication and Authorization for security implementation details.

The best APIs are those that developers enjoy using. Focus on consistency, clarity, and comprehensive documentation, and your API will become a valuable asset for your platform.