system-design · intermediate

API Design — Clear Contracts Clients Can Trust

The Central Question

Consider an e-commerce platform running on the APILab platform (apilab.com) where different backend teams implemented endpoints independently:


As the engineering team grows to 50 developers building 10 separate client applications (iOS, Android, Web, Partner Integrations), this ad-hoc lack of design standards causes endless integration bugs, inconsistent client error handling, and accidental database overload.

API Design is the disciplined architectural practice of structuring API resources, HTTP methods, data schemas, error payloads, and pagination mechanisms to create predictable, maintainable software contracts.

This lesson answers one central question: How do engineers design RESTful resource hierarchies, select HTTP methods, structure actionable error schemas, and implement pagination models to build stable, developer-friendly APIs?


The Core Principles of RESTful API Design

The most prevalent API design paradigm for web and mobile backends is REST (Representational State Transfer), which models system functionality around Resources accessed via standard HTTP methods.

flowchart TD
  subgraph Nouns: Identifiable System Resources
    Users["/v1/users (User Accounts)"]
    Orders["/v1/orders (Customer Orders)"]
    Products["/v1/products (Catalog Items)"]
  end
  subgraph Verbs: Standard HTTP Methods
    GET["GET: Read Resource State"]
    POST["POST: Create New Resource"]
    PUT["PUT: Replace Entire Resource"]
    PATCH["PATCH: Update Partial Resource"]
    DELETE["DELETE: Remove Resource"]
  end
  
  Users -->|Exposes| GET
  Users -->|Exposes| POST
  Orders -->|Exposes| PUT
  Orders -->|Exposes| PATCH
  Products -->|Exposes| DELETE

Figure 1: Decoupling resource nouns (URIs) from operation verbs (HTTP methods).

1. Model URIs Around Nouns, Not Verbs

API URIs should identify **resources** (nouns) rather than remote procedural actions (verbs):

2. Standardize Plural Noun Conventions

Use plural nouns for resource collections (`/users`, `/orders`, `/products`) to maintain consistency across endpoints:

HTTP Method Semantics: Safety and Idempotency

In disciplined API design, every HTTP method conveys strict operational semantics regarding Safety and Idempotency:

flowchart LR
  subgraph Safe & Idempotent (Read Only)
    GET[GET /v1/products]
    HEAD[HEAD /v1/products]
  end
  subgraph Idempotent but Unsafe (Mutates State)
    PUT[PUT /v1/users/42]
    DELETE[DELETE /v1/users/42]
  end
  subgraph Neither Safe nor Idempotent
    POST[POST /v1/orders]
  end

Figure 2: Taxonomy of HTTP methods categorized by Safety and Idempotency properties.

Safety vs. Idempotency Definitions

HTTP Method Properties Matrix

MethodIntended ActionSafe?Idempotent?Typical Success Status Code
GETRetrieve resource state.YesYes200 OK
POSTCreate a new resource or execute a complex operation.NoNo201 Created
PUTReplace an entire resource payload (full overwrite).NoYes200 OK / 204 No Content
PATCHApply partial modifications to an existing resource.NoNo200 OK
DELETERemove a specified resource.NoYes204 No Content

Structuring Actionable Error Payloads: RFC 7807

A major cause of client integration failure is inconsistent error payloads. One endpoint returns {"err": "missing_field"}, while another returns an un-parsed stack trace.

High-quality API design standardizes all error responses on RFC 7807 (Problem Details for HTTP APIs).

flowchart TB
  ErrResp[HTTP 400 Bad Request Response] --> Type["type: URI pointing to error documentation"]
  ErrResp --> Title["title: Short human summary"]
  ErrResp --> Status["status: HTTP status code (e.g. 400)"]
  ErrResp --> Detail["detail: Specific explanation of this failure"]
  ErrResp --> InvalidParams["invalidParams: Array of specific field errors"]

Figure 3: Anatomy of an RFC 7807 compliant JSON error detail payload.

Production RFC 7807 Error Payload Example

{
  "type": "https://api.apilab.com/errors/validation-error",
  "title": "Invalid Request Parameters",
  "status": 400,
  "detail": "The request payload failed validation against the order schema.",
  "instance": "/v1/orders/err_99102",
  "invalidParams": [
    {
      "name": "items[0].quantity",
      "reason": "Quantity must be an integer between 1 and 100."
    },
    {
      "name": "shippingAddressId",
      "reason": "Address ID 'addr_00' does not exist for this user."
    }
  ]
}

Why Machine-Readable Error Codes Matter

Clients branch logic based on machine-readable codes (such as `type` URIs or specific error code enums), not free-text human strings. If the backend team updates the human `detail` text from `"Invalid qty"` to `"Quantity out of range"`, client branching logic remains unbroken.

Collection Pagination: Offset vs. Cursor Models

List endpoints (GET /v1/orders) that return thousands of records must implement Pagination. Un-paginated list endpoints cause database memory exhaustion and high latency.

flowchart TB
  subgraph Offset-Based Pagination
    Off1["GET /v1/orders?offset=40&limit=20"] --> OffDB["SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 40"]
    Note1["Issue: Performance degrades to O(N); page drift when items inserted"]
  end
  subgraph Cursor-Based Pagination
    Cur1["GET /v1/orders?cursor=ord_9940&limit=20"] --> CurDB["SELECT * FROM orders WHERE id > 'ord_9940' ORDER BY id LIMIT 20"]
    Note2["Benefit: Fast O(1) indexed lookup; immune to page drift"]
  end

Figure 4: Comparing offset-based SQL scans against indexed cursor-based pagination.

1. Offset-Based Pagination (offset & limit)

The client passes an offset count and page limit (`GET /v1/orders?offset=100&limit=20`). - *O(N) Database Performance*: SQL databases must scan and discard $N$ rows before returning results. Offset 1,000,000 is extremely slow. - *Page Drift*: If a new record is inserted at row 1 while a user is on Page 1, moving to Page 2 causes them to see duplicate records.

2. Cursor-Based Pagination (cursor & limit)

The client passes an opaque pointer (typically a base64-encoded string containing a timestamp or unique ID) representing the last seen item (`GET /v1/orders?cursor=eyJpZCI6OSw...&limit=20`). - *O(1) Database Performance*: SQL utilizes primary key or timestamp indexes (`WHERE id > cursor LIMIT 20`), executing instantly regardless of table size. - *Immune to Page Drift*: Inserting or deleting rows does not alter the relative position of the cursor pointer.

Pagination Comparison Matrix

Operational VectorOffset-Based PaginationCursor-Based Pagination
SQL Query PatternLIMIT 20 OFFSET 1000WHERE id > :cursor LIMIT 20
Database Execution Time$O(N)$ — Slow on large datasets.$O(1)$ — Fast index range scan.
Data ConsistencyProne to page drift & missing items.Perfectly stable during real-time writes.
UI CompatibilityIdeal for numbered page links (Page 1, 2, 3).Ideal for infinite scroll mobile feeds.

API Versioning Strategies: Path vs. Header vs. Query Parameters

When shipping breaking changes, backend engineering teams choose between three primary API versioning strategies:

flowchart TD
  Version[API Versioning Strategies] --> Path["1. URI Path Versioning (/v1/orders vs /v2/orders)"]
  Version --> Header["2. Custom Request Header (X-API-Version: 2026-07-24)"]
  Version --> Accept["3. Accept Header Content Negotiation (Accept: application/vnd.company.v2+json)"]
  
  Path --> PathAdv["Explicit in browser & logs; easy edge gateway routing."]
  Header --> HeaderAdv["Clean URIs; allows date-based API evolution (Stripe style)."]
  Accept --> AcceptAdv["Strict REST conformance; complex client configuration."]

Figure 5: Taxonomical comparison of API versioning implementation patterns.

1. URI Path Versioning (/v1/orders)

The major version number is embedded directly into the URI path (`https://api.apilab.com/v1/orders`).

Complete Worked Example: Go Cursor-Paginated REST API

Let's inspect a production Go implementation of a RESTful collection handler for the APILab platform (apilab.com) demonstrating cursor-based pagination and RFC 7807 error responses.

package main

import (
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
)

type Product struct {
ID string json:"id"
Name string json:"name"
Price float64 json:"price"
}

type PageInfo struct {
NextCursor string json:"nextCursor"
HasNextPage bool json:"hasNextPage"
}

type PaginatedProductsResponse struct {
Data []Product json:"data"
PageInfo PageInfo json:"pageInfo"
}

func ProductsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

limitStr := r.URL.Query().Get("limit")
limit := 20
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}

cursor := r.URL.Query().Get("cursor")
lastID := ""
if cursor != "" {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err == nil {
lastID = string(decoded)
}
}

// Execute O(1) indexed SQL Query: SELECT id, name, price FROM products WHERE id > $1 ORDER BY id ASC LIMIT $2
products := fetchProductsFromDB(lastID, limit+1)

hasNext := false
if len(products) > limit {
hasNext = true
products = products[:limit]
}

nextCursor := ""
if hasNext && len(products) > 0 {
lastProduct := products[len(products)-1]
nextCursor = base64.StdEncoding.EncodeToString([]byte(lastProduct.ID))
}

res := PaginatedProductsResponse{
Data: products,
PageInfo: PageInfo{
NextCursor: nextCursor,
HasNextPage: hasNext,
},
}

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
}

func fetchProductsFromDB(lastID string, limit int) []Product {
// Database query placeholder...
return []Product{
{ID: "prod_101", Name: "Mechanical Keyboard", Price: 129.99},
{ID: "prod_102", Name: "Wireless Mouse", Price: 59.99},
}
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Un-paginated List EndpointsGET /v1/orders executes SELECT * FROM orders without limits.Database memory exhaustion (OOM crash) during peak query traffic.High database memory utilization and long p99 response times on list endpoints.Enforce mandatory default (limit=20) and maximum (limit=100) page size caps at the API layer.
2. Deep Offset Pagination SlowdownClient queries GET /v1/catalog?offset=500000.Database CPU hits 100% as SQL engine scans 500,000 un-indexed rows.High database lock wait times and slow query log alerts.Migrate high-volume collection endpoints from offset-based to cursor-based pagination.
3. Non-Standard Error FormattingMicroservices return custom error fields (errCode, error_message, msg).Mobile client crash rate increases due to missing error field handling.High client-side exception metrics following API release.Standardize all backend microservices on RFC 7807 Problem Details error middleware.
4. Verb Pollution in REST URIsExposing endpoints like /v1/cancelOrder or /v1/updateUser.Inconsistent URI routing; cache invalidation rules break at edge gateways.Inconsistent routing metrics and CDN cache miss rates.Model URIs around plural resources (PATCH /v1/orders/ord_881) with state payload transitions (&quot;status&quot;: &quot;CANCELLED&quot;).

What You Should Remember

  1. Nouns for URIs, Verbs for HTTP Methods: Name resources with plural nouns (/v1/orders). Use HTTP methods (GET, POST, PUT, DELETE) to express operations.
  2. Respect Safety and Idempotency: GET must be safe and read-only. PUT and DELETE must be idempotent. POST is neither safe nor idempotent by default.
  3. Standardize on RFC 7807 Error Schemas: Return machine-readable error codes (type, status, detail, invalidParams) so clients can branch logic reliably.
  4. Prefer Cursor-Based Pagination for Scale: Use opaque cursor pointers (WHERE id > cursor LIMIT 20) to maintain $O(1)$ database query performance and prevent page drift.
  5. Enforce Hard Limits on Collections: Never expose an un-paginated collection endpoint. Set mandatory default and maximum page limits.

Glossary of Terms

TermDefinition
API DesignThe practice of structuring resource URIs, methods, data schemas, and error handling for APIs.
REST (Representational State Transfer)An architectural style that models system functionality around resources accessed via standard HTTP verbs.
ResourceAn entity or collection exposed by an API (e.g. /v1/users, /v1/orders).
Idempotent MethodAn HTTP operation where repeating the request produces the exact same final server state as executing it once.
Safe MethodAn HTTP operation that reads data without causing state mutations on the server.
RFC 7807The standard JSON specification for communicating HTTP API error details.
Cursor PaginationPaginating dataset results using an indexed pointer rather than a numeric offset count.
Page DriftThe phenomenon where inserted or deleted rows cause users to skip or see duplicate records during offset pagination.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing an API for an online learning platform (`learnlab.com`). Students can enroll in courses, track progress, and submit assignments.

A junior developer proposes the following endpoints:

  1. POST /api/getStudentCourses?studentId=42
  2. POST /api/deleteEnrollment?enrollmentId=90
  3. GET /api/getAllAssignments (returns all 800,000 assignments in one JSON array)

Questions:
  1. Identify the API design flaws in each of the three proposed endpoints.
  2. Refactor all three endpoints into a clean RESTful specification adhering to REST resource conventions, proper HTTP verbs, and cursor pagination.


Interactive Self-Assessment

cursor), providing fast O(1) database performance and preventing page drift during active writes.">It uses indexed range scans for O(1) database performance and eliminates page drift during concurrent writes.

It allows clients to jump directly to arbitrary page numbers (such as Page 500) faster.

It automatically compresses HTTP JSON payloads using Gzip compression.

It eliminates the need for HTTP status codes on collection endpoints.

The machine-readable 'type' URI code indicating the specific error category.

The human-readable 'detail' text string explaining the error.

The generic HTTP 400 status code alone.

The 'instance' request trace ID assigned to the execution.


What to Learn Next

Track: Software Design and Architecture

Next: Distributed Rate Limiting — Shared Quotas Across Many Pods

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab