Skip to main content
REST API design architecture and best practices
Blog

REST API Design Best Practices 2026: Build APIs Developers Love

Por Ramon Nuila lunes, 15 de diciembre de 2025 · 24 min de lectura

Complete guide to REST API design best practices in 2026. Learn URL structure, HTTP methods, status codes, versioning, pagination, error handling, and documentation. Build professional, maintainable APIs.

REST API Design Best Practices 2026: Build APIs Developers Love

A well-designed API is a joy to use. A poorly designed one creates frustration, bugs, and endless support tickets. The difference often comes down to following established patterns and conventions.

This guide covers everything you need to design REST APIs that developers actually enjoy working with—clear URL structures, proper HTTP methods, meaningful status codes, and robust error handling.


Part 1: URL Structure

1. Resource Naming

URLs should represent resources (nouns), not actions (verbs).

Good URL Design:

GET    /users              # List all users
GET    /users/123          # Get user 123
POST   /users              # Create a new user
PUT    /users/123          # Update user 123
DELETE /users/123          # Delete user 123

GET    /users/123/orders   # List orders for user 123
GET    /orders/456         # Get order 456

Bad URL Design:

GET    /getUsers           # Verb in URL
POST   /createUser         # Verb in URL
GET    /getUserById?id=123 # Query param for resource ID
POST   /deleteUser/123     # Wrong HTTP method

2. Plural vs Singular

Use plural nouns for consistency:

✅ /users        (not /user)
✅ /products     (not /product)
✅ /orders       (not /order)
✅ /categories   (not /category)

3. Hierarchical Resources

Express relationships through URL hierarchy:

# User's orders
GET /users/123/orders

# Specific order for a user
GET /users/123/orders/456

# Order items
GET /orders/456/items

# Alternative: filter approach (also valid)
GET /orders?user_id=123

When to Use Hierarchy vs Query Parameters:

Use Hierarchy WhenUse Query Params When
Strong parent-child relationshipFiltering/searching
Resource can’t exist without parentOptional criteria
Always accessed in contextCross-resource queries

4. URL Conventions

# Use lowercase
✅ /users/123/orders
❌ /Users/123/Orders

# Use hyphens for multi-word resources
✅ /user-profiles
❌ /userProfiles
❌ /user_profiles

# No trailing slashes
✅ /users/123
❌ /users/123/

# No file extensions
✅ /users/123
❌ /users/123.json

Part 2: HTTP Methods

5. Method Semantics

Each HTTP method has specific semantics—use them correctly:

MethodPurposeIdempotentSafeRequest Body
GETRetrieve resourceYesYesNo
POSTCreate resourceNoNoYes
PUTReplace resourceYesNoYes
PATCHPartial updateYes*NoYes
DELETERemove resourceYesNoOptional

Idempotent: Same request repeated = same result Safe: Doesn’t modify server state

6. Method Usage Examples

GET - Retrieve Resources:

GET /users HTTP/1.1
Host: api.example.com
Authorization: Bearer token123

# Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": [
    { "id": 1, "name": "John", "email": "john@example.com" },
    { "id": 2, "name": "Jane", "email": "jane@example.com" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20
  }
}

POST - Create Resource:

POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123

{
  "name": "New User",
  "email": "newuser@example.com",
  "role": "member"
}

# Response
HTTP/1.1 201 Created
Location: /users/3
Content-Type: application/json

{
  "data": {
    "id": 3,
    "name": "New User",
    "email": "newuser@example.com",
    "role": "member",
    "createdAt": "2026-01-15T10:30:00Z"
  }
}

PUT - Replace Resource:

PUT /users/3 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "name": "Updated User",
  "email": "updated@example.com",
  "role": "admin"
}

# Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "id": 3,
    "name": "Updated User",
    "email": "updated@example.com",
    "role": "admin",
    "updatedAt": "2026-01-15T11:00:00Z"
  }
}

PATCH - Partial Update:

PATCH /users/3 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "role": "admin"
}

# Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "id": 3,
    "name": "Updated User",
    "email": "updated@example.com",
    "role": "admin",
    "updatedAt": "2026-01-15T11:05:00Z"
  }
}

DELETE - Remove Resource:

DELETE /users/3 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123

# Response
HTTP/1.1 204 No Content

Part 3: HTTP Status Codes

7. Success Codes (2xx)

CodeNameWhen to Use
200OKGET, PUT, PATCH success
201CreatedPOST created new resource
204No ContentDELETE success, no body
202AcceptedAsync processing started

8. Client Error Codes (4xx)

CodeNameWhen to Use
400Bad RequestInvalid request body/params
401UnauthorizedMissing/invalid authentication
403ForbiddenAuthenticated but not allowed
404Not FoundResource doesn’t exist
405Method Not AllowedWrong HTTP method
409ConflictResource conflict (duplicate)
422Unprocessable EntityValidation errors
429Too Many RequestsRate limit exceeded

9. Server Error Codes (5xx)

CodeNameWhen to Use
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service failed
503Service UnavailableTemporarily unavailable
504Gateway TimeoutUpstream timeout

10. Status Code Decision Tree

Request received

    ├── Valid authentication?
    │   ├── No → 401 Unauthorized
    │   └── Yes → Continue

    ├── Has permission?
    │   ├── No → 403 Forbidden
    │   └── Yes → Continue

    ├── Resource exists? (for GET/PUT/PATCH/DELETE)
    │   ├── No → 404 Not Found
    │   └── Yes → Continue

    ├── Request valid?
    │   ├── No → 400 Bad Request
    │   └── Yes → Continue

    ├── Validation passes?
    │   ├── No → 422 Unprocessable Entity
    │   └── Yes → Continue

    ├── Business rules pass?
    │   ├── No → 409 Conflict (or appropriate 4xx)
    │   └── Yes → Continue

    └── Process request
        ├── Success → 200/201/204
        └── Error → 500

Part 4: Request & Response Design

11. Request Body Structure

Consistent JSON Structure:

// POST /users
{
  "name": "John Doe",
  "email": "john@example.com",
  "profile": {
    "bio": "Software developer",
    "avatar": "https://example.com/avatar.jpg"
  },
  "preferences": {
    "newsletter": true,
    "theme": "dark"
  }
}

Conventions:

  • Use camelCase for property names
  • Use ISO 8601 for dates: 2026-01-15T10:30:00Z
  • Use consistent null handling
  • Avoid deeply nested structures (3 levels max)

12. Response Envelope

Wrap responses in a consistent structure:

// Success response
{
  "data": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-01-15T10:30:00Z"
  }
}

// List response
{
  "data": [
    { "id": 1, "name": "John" },
    { "id": 2, "name": "Jane" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "totalPages": 5
  },
  "links": {
    "self": "/users?page=1",
    "next": "/users?page=2",
    "last": "/users?page=5"
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "name",
        "message": "Name is required"
      }
    ]
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-01-15T10:30:00Z"
  }
}

13. TypeScript Implementation

// Response types
interface ApiResponse<T> {
  data: T;
  meta: ResponseMeta;
}

interface ListResponse<T> {
  data: T[];
  meta: ListMeta;
  links: PaginationLinks;
}

interface ErrorResponse {
  error: {
    code: string;
    message: string;
    details?: ValidationError[];
  };
  meta: ResponseMeta;
}

interface ResponseMeta {
  requestId: string;
  timestamp: string;
}

interface ListMeta extends ResponseMeta {
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

interface PaginationLinks {
  self: string;
  first?: string;
  prev?: string;
  next?: string;
  last?: string;
}

// Usage
function success<T>(data: T, meta?: Partial<ResponseMeta>): ApiResponse<T> {
  return {
    data,
    meta: {
      requestId: generateRequestId(),
      timestamp: new Date().toISOString(),
      ...meta,
    },
  };
}

function error(code: string, message: string, details?: ValidationError[]): ErrorResponse {
  return {
    error: { code, message, details },
    meta: {
      requestId: generateRequestId(),
      timestamp: new Date().toISOString(),
    },
  };
}

Part 5: Pagination, Filtering & Sorting

14. Pagination

Offset-Based Pagination (Simple):

GET /users?page=2&limit=20

# Response
{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 2,
    "limit": 20,
    "totalPages": 5
  },
  "links": {
    "first": "/users?page=1&limit=20",
    "prev": "/users?page=1&limit=20",
    "self": "/users?page=2&limit=20",
    "next": "/users?page=3&limit=20",
    "last": "/users?page=5&limit=20"
  }
}

Cursor-Based Pagination (Better for Large Datasets):

GET /users?cursor=eyJpZCI6MTAwfQ&limit=20

# Response
{
  "data": [...],
  "meta": {
    "limit": 20,
    "hasMore": true
  },
  "cursors": {
    "next": "eyJpZCI6MTIwfQ",
    "prev": "eyJpZCI6MTAwfQ"
  }
}

When to Use Each:

Pagination TypeUse When
OffsetSmall datasets, need page numbers
CursorLarge datasets, real-time data, infinite scroll

15. Filtering

Use query parameters for filtering:

# Single filter
GET /products?category=electronics

# Multiple filters
GET /products?category=electronics&brand=apple&minPrice=100&maxPrice=1000

# Array filter
GET /products?tags=featured,sale,new

# Date range
GET /orders?createdAfter=2026-01-01&createdBefore=2026-01-31

# Search
GET /products?search=iphone

Implementation:

interface ProductFilters {
  category?: string;
  brand?: string;
  minPrice?: number;
  maxPrice?: number;
  tags?: string[];
  search?: string;
  createdAfter?: Date;
  createdBefore?: Date;
}

function buildProductQuery(filters: ProductFilters) {
  let query = db.select().from(products);

  if (filters.category) {
    query = query.where(eq(products.category, filters.category));
  }

  if (filters.minPrice) {
    query = query.where(gte(products.price, filters.minPrice));
  }

  if (filters.maxPrice) {
    query = query.where(lte(products.price, filters.maxPrice));
  }

  if (filters.search) {
    query = query.where(
      or(
        like(products.name, `%${filters.search}%`),
        like(products.description, `%${filters.search}%`)
      )
    );
  }

  return query;
}

16. Sorting

# Single sort
GET /products?sort=price

# Descending sort
GET /products?sort=-price

# Multiple sorts
GET /products?sort=-createdAt,name

# Alternative syntax
GET /products?sortBy=price&sortOrder=desc

Implementation:

function applySorting(query: Query, sort: string) {
  const fields = sort.split(',');

  for (const field of fields) {
    const isDescending = field.startsWith('-');
    const fieldName = isDescending ? field.slice(1) : field;

    if (isDescending) {
      query = query.orderBy(desc(products[fieldName]));
    } else {
      query = query.orderBy(asc(products[fieldName]));
    }
  }

  return query;
}

Part 6: Versioning

17. Versioning Strategies

URL Path Versioning (Recommended):

GET /v1/users
GET /v2/users

Pros: Clear, easy to implement, cacheable Cons: URL changes between versions

Header Versioning:

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

Pros: Clean URLs Cons: Harder to test, less visible

Query Parameter Versioning:

GET /users?version=2

Pros: Easy to implement Cons: Not RESTful, caching issues

18. Version Implementation

// Express.js route organization
// /routes/v1/users.ts
const v1Router = express.Router();

v1Router.get('/users', async (req, res) => {
  // V1 implementation
});

// /routes/v2/users.ts
const v2Router = express.Router();

v2Router.get('/users', async (req, res) => {
  // V2 implementation with breaking changes
});

// app.ts
app.use('/v1', v1Router);
app.use('/v2', v2Router);

Part 7: Error Handling

19. Error Response Format

interface ApiError {
  error: {
    code: string;           // Machine-readable code
    message: string;        // Human-readable message
    details?: ErrorDetail[]; // Specific errors
    help?: string;          // Link to documentation
  };
}

interface ErrorDetail {
  field?: string;
  code: string;
  message: string;
}

Example Error Responses:

// 400 Bad Request
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "The request body is not valid JSON",
    "help": "https://docs.api.com/errors/invalid-request"
  }
}

// 422 Validation Error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "One or more fields failed validation",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Email must be a valid email address"
      },
      {
        "field": "password",
        "code": "TOO_SHORT",
        "message": "Password must be at least 8 characters"
      }
    ]
  }
}

// 404 Not Found
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "User with ID 123 not found"
  }
}

// 429 Rate Limited
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please try again in 60 seconds",
    "details": [
      {
        "code": "RETRY_AFTER",
        "message": "60"
      }
    ]
  }
}

20. Error Handler Implementation

// Custom error classes
class ApiError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: ErrorDetail[]
  ) {
    super(message);
  }
}

class ValidationError extends ApiError {
  constructor(details: ErrorDetail[]) {
    super(422, 'VALIDATION_ERROR', 'Validation failed', details);
  }
}

class NotFoundError extends ApiError {
  constructor(resource: string, id: string | number) {
    super(404, 'RESOURCE_NOT_FOUND', `${resource} with ID ${id} not found`);
  }
}

// Global error handler
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  // Log error for debugging
  console.error(err);

  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        details: err.details,
      },
    });
  }

  // Unknown error - don't expose details
  return res.status(500).json({
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
    },
  });
}

// Usage
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await findUser(req.params.id);

    if (!user) {
      throw new NotFoundError('User', req.params.id);
    }

    res.json({ data: user });
  } catch (error) {
    next(error);
  }
});

Part 8: Authentication & Security

21. Authentication Methods

Bearer Token (Recommended):

GET /users HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

API Key:

GET /users HTTP/1.1
X-API-Key: sk_live_xxxxxxxxxxxxx

22. Security Headers

// Security middleware
app.use((req, res, next) => {
  // Prevent caching of authenticated responses
  res.set('Cache-Control', 'no-store');
  res.set('Pragma', 'no-cache');

  // Security headers
  res.set('X-Content-Type-Options', 'nosniff');
  res.set('X-Frame-Options', 'DENY');

  next();
});

23. Rate Limiting

import rateLimit from 'express-rate-limit';

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  handler: (req, res) => {
    res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests, please try again later',
      },
    });
  },
});

app.use('/api/', apiLimiter);

Part 9: Documentation

24. OpenAPI/Swagger

openapi: 3.0.3
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
            maximum: 100
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'

    post:
      summary: Create a user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'

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
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email

    UserResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/User'

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

security:
  - bearerAuth: []

Part 10: API Design Checklist

Before Publishing Your API

URL Design:

  • Use plural nouns for resources
  • Use lowercase with hyphens
  • Hierarchical structure for relationships
  • No trailing slashes

HTTP Methods:

  • GET for retrieval (no body)
  • POST for creation
  • PUT for full replacement
  • PATCH for partial updates
  • DELETE for removal

Status Codes:

  • 200/201/204 for success
  • 400 for bad requests
  • 401 for authentication issues
  • 403 for authorization issues
  • 404 for not found
  • 422 for validation errors

Response Format:

  • Consistent envelope structure
  • Meaningful error messages
  • Pagination for lists
  • ISO 8601 dates

Security:

  • HTTPS only
  • Authentication implemented
  • Rate limiting
  • Input validation

Documentation:

  • OpenAPI spec
  • Example requests/responses
  • Error code reference

How Codebrand Builds APIs

At Codebrand, we apply these best practices to every API we build. Our clients get APIs that are easy to use, well-documented, and built to scale.

Our API Development Services

  • API Design: Architect clean, intuitive APIs from scratch
  • API Development: Build robust, secure APIs with modern technologies
  • API Documentation: Create comprehensive documentation developers love
  • API Integration: Connect your systems with third-party APIs
  • API Optimization: Improve performance and scalability of existing APIs

Technologies We Use

  • Frameworks: Node.js, Astro, Next.js
  • Databases: Turso, PostgreSQL, MongoDB
  • Documentation: OpenAPI/Swagger, Redoc
  • Hosting: Cloudflare Workers, Netlify Functions, Vercel

Need a professional API for your project?

Contact us for a free consultation and let’s discuss how we can build an API developers will love.


Have questions about API design? Reach out to our team—we’re passionate about building great APIs.

Do you want to read more articles?

Visit our blog to explore more content on web development, design, and digital marketing.

Read More Articles