Software Teams

API Design Patterns for Modern Web Applications

Practical API design patterns covering resource modeling, error handling, pagination, filtering, and versioning for production APIs.

Designing APIs That Last

The difference between an API that developers love and one they tolerate comes down to consistency and predictability. A well-designed API follows patterns that, once learned, apply everywhere. A poorly designed API requires reading documentation for every single endpoint.

This guide covers the patterns that matter most when building APIs consumed by frontend teams, mobile apps, and third-party integrations.

Resource Modeling

Your API resources should map to business concepts, not database tables. If your customers table has 40 columns but API consumers only care about 12 fields, your resource should expose those 12 fields.

Naming conventions that work:

  • Use plural nouns for collections: /invoices, /customers, /subscriptions
  • Use nested resources for clear ownership: /customers/42/invoices
  • Avoid deep nesting beyond two levels. Instead of /customers/42/invoices/17/lines/3, use /invoice-lines/3 with filter parameters
  • Use kebab-case for multi-word resources: /payment-methods, not /paymentMethods

Actions that do not fit CRUD are the exception to noun-based URLs. Use a verb: POST /invoices/42/finalize or POST /subscriptions/7/cancel. Keep these rare.

Request and Response Design

Consistent Response Envelope

Pick one response structure and use it everywhere:

{
  "data": {
    "id": 42,
    "type": "invoice",
    "attributes": {
      "number": "INV-2026-0042",
      "total": "1250.00",
      "currency": "EUR",
      "status": "finalized"
    }
  }
}

For collections:

{
  "data": [
    { "id": 42, "type": "invoice", "attributes": { ... } },
    { "id": 43, "type": "invoice", "attributes": { ... } }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 148
  }
}

Whether you use JSON:API format, a custom envelope, or something else matters less than using it consistently across every endpoint.

Field Selection

Let consumers request only the fields they need:

GET /invoices?fields=id,number,total,status

This reduces payload size dramatically for mobile clients and list views. Implement it once as middleware and it works across all resources.

Including Related Resources

Avoid the N+1 problem at the API level. Let consumers request related resources in one call:

GET /invoices/42?include=customer,lines.product

This pattern (popularized by JSON:API) prevents consumers from making dozens of requests to assemble a single view. Limit the depth and allowed includes to prevent performance issues.

Error Handling

Errors are where most APIs fall apart. The consumer receives a 500 status with a generic message and has no idea what went wrong.

Structured error responses:

{
  "error": {
    "type": "validation_error",
    "message": "The request could not be processed due to validation errors.",
    "errors": {
      "customer_id": ["The selected customer does not exist."],
      "lines": ["At least one invoice line is required."]
    }
  }
}

Status code discipline:

  • 200 for successful GET, PUT, PATCH
  • 201 for successful POST that creates a resource
  • 204 for successful DELETE
  • 400 for malformed requests
  • 401 for missing or invalid authentication
  • 403 for valid authentication but insufficient permissions
  • 404 for resources that do not exist
  • 409 for conflict (duplicate, state conflict)
  • 422 for validation errors (request is well-formed but semantically invalid)
  • 429 for rate limit exceeded
  • 500 for server errors (never expose internal details)

Include a machine-readable error type alongside the human-readable message. This lets consumers handle errors programmatically without parsing strings.

Pagination Patterns

Offset-Based Pagination

GET /invoices?page=3&per_page=25

Simple to implement and understand. Works well for most use cases. Breaks down with very large datasets because OFFSET 10000 is expensive in most databases.

Cursor-Based Pagination

GET /invoices?cursor=eyJpZCI6NDJ9&per_page=25

The cursor encodes the position in the result set (typically the last seen ID). Performs consistently regardless of dataset size because it translates to a WHERE id > 42 clause instead of an offset.

Use cursor-based pagination for feeds, activity logs, and any resource where consumers scroll through large datasets. Use offset-based for admin panels and dashboards where jumping to a specific page matters.

Filtering and Sorting

Provide consistent filtering across resources:

GET /invoices?filter[status]=finalized&filter[customer_id]=42&filter[created_after]=2026-01-01

For sorting:

GET /invoices?sort=-created_at,number

The minus prefix indicates descending order. Document available filter and sort fields per resource.

Rate Limiting

Every production API needs rate limiting. Communicate limits clearly in response headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1706140800

When the limit is exceeded, return 429 Too Many Requests with a Retry-After header. Differentiate rate limits by authentication level: anonymous requests get stricter limits than authenticated API key requests.

Idempotency

POST requests should support idempotency for critical operations. The consumer sends an Idempotency-Key header:

POST /invoices
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890

If the server has already processed a request with this key, it returns the original response instead of creating a duplicate. This is essential for payment operations, order creation, and any action with real-world side effects.

Store idempotency keys with their responses for 24-48 hours. This prevents duplicate processing from network retries, user double-clicks, and mobile connectivity issues.

Authentication Patterns

For first-party frontends, use session-based authentication with CSRF tokens. For third-party integrations and mobile apps, use OAuth 2.0 with short-lived access tokens and refresh tokens.

API keys (bearer tokens) work for server-to-server communication where the client is a trusted backend service. Never use API keys in browser-based applications since they cannot be kept secret on the client.

Documentation Is Part of the API

An undocumented API is an unusable API. Generate documentation from your code using OpenAPI specifications. Keep examples realistic (not "string" and "0" placeholders). Include error responses in your documentation, not just happy paths.

The best API documentation includes runnable examples that consumers can copy, paste, and adapt. If your documentation requires 30 minutes of setup before making the first request, you have lost most of your audience.

Let's talk about your software teams needs

Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.

Book a 30-min Call