Web APIs: A Complete Guide to REST, GraphQL, and Modern API Design in 2026

February 11, 2026 · DevToolbox Team

Every modern application is built on APIs. Whether you are fetching data in a React frontend, integrating a payment provider, pulling metrics from a cloud service, or connecting microservices behind the scenes, you are using web APIs. They are the contracts that allow software systems to communicate, and understanding how to design, consume, and secure them is a core skill for every developer.

This guide covers everything you need to know about web APIs in 2026: from foundational REST principles to GraphQL queries, from authentication mechanisms like OAuth 2.0 and JWT to modern protocols like gRPC and WebSockets. Whether you are building your first API or refining an existing one, you will leave with practical knowledge you can apply immediately.

⚙ Try it: Test your API endpoints with our HTTP Request Tester, format API responses with the JSON Formatter, and decode tokens with the JWT Decoder.

What Are Web APIs and Why Do They Matter?

An API (Application Programming Interface) is a set of rules that defines how two pieces of software communicate. A web API specifically uses HTTP as its transport protocol, making it accessible from any device or language that can make HTTP requests. When your browser loads a social media feed, it is calling a web API. When your CI/CD pipeline triggers a deployment, it is calling a web API. When your phone shows the weather forecast, a web API delivered that data.

Web APIs matter because they enable decoupling. Instead of building monolithic applications where every feature is tightly wired together, APIs allow teams to build independent services that communicate through well-defined interfaces. A frontend team can build against an API contract without waiting for the backend to be finished. A mobile team and a web team can share the same API. Third-party developers can extend your platform without touching your source code.

The API economy is enormous. Companies like Stripe, Twilio, and SendGrid built billion-dollar businesses on APIs alone. Salesforce generates over 50% of its revenue through its API. In 2026, Postman's State of APIs report found that over 75% of organizations consider APIs critical to their business strategy. Understanding API design is not optional; it is a career necessity.

REST APIs: The Foundation of Web Services

REST (Representational State Transfer) is an architectural style defined by Roy Fielding in his 2000 doctoral dissertation. It is not a protocol or a specification but a set of constraints that, when followed, produce scalable and maintainable web services. REST has been the dominant API paradigm for over two decades and remains the most widely used approach in 2026.

Core REST Principles

A truly RESTful API adheres to six architectural constraints:

  1. Client-Server Separation: The client and server are independent. The client does not need to know how the server stores data, and the server does not need to know about the client's UI. This separation allows each side to evolve independently.
  2. Statelessness: Every request from the client must contain all the information needed to process it. The server does not store session state between requests. This makes APIs easier to scale because any server in a cluster can handle any request.
  3. Cacheability: Responses must explicitly indicate whether they can be cached. Proper caching reduces server load and improves client performance. HTTP cache headers (Cache-Control, ETag, Last-Modified) are the mechanism for this.
  4. Uniform Interface: The API uses a consistent, standardized interface. Resources are identified by URIs. Representations (JSON, XML) are used to manipulate resources. Messages are self-descriptive. Hypermedia links drive application state (HATEOAS).
  5. Layered System: The client cannot tell whether it is connected directly to the server or to an intermediary like a load balancer, CDN, or API gateway. This enables infrastructure flexibility without changing the API.
  6. Code on Demand (Optional): Servers can extend client functionality by transferring executable code (like JavaScript). This is the only optional constraint.

HTTP Methods in REST

REST APIs use standard HTTP methods (verbs) to perform operations on resources. Each method has specific semantics that clients and servers agree on:

# GET — Retrieve a resource (safe, idempotent)
GET /api/v1/users/42
# Returns the user with ID 42

# GET — List resources with filtering
GET /api/v1/users?role=admin&status=active&page=1&limit=20
# Returns a filtered, paginated list of users

# POST — Create a new resource (not idempotent)
POST /api/v1/users
Content-Type: application/json

{
  "name": "Alice Chen",
  "email": "alice@example.com",
  "role": "developer"
}
# Server creates the user and returns 201 Created

# PUT — Replace a resource entirely (idempotent)
PUT /api/v1/users/42
Content-Type: application/json

{
  "name": "Alice Chen",
  "email": "alice.chen@example.com",
  "role": "senior-developer"
}
# Replaces ALL fields of user 42

# PATCH — Partially update a resource (not always idempotent)
PATCH /api/v1/users/42
Content-Type: application/json

{
  "role": "senior-developer"
}
# Updates ONLY the role field, leaving everything else unchanged

# DELETE — Remove a resource (idempotent)
DELETE /api/v1/users/42
# Deletes user 42, returns 204 No Content

The distinction between PUT and PATCH is important and frequently misunderstood. PUT replaces the entire resource, meaning any field you omit will be removed or set to its default. PATCH updates only the fields you include. In practice, most APIs implement PATCH for updates because it is more bandwidth-efficient and less error-prone.

⚙ Test it yourself: Use the HTTP Request Tester to send GET, POST, PUT, PATCH, and DELETE requests to any API endpoint and inspect the full response including headers and status codes.

HTTP Status Codes

Status codes tell the client what happened with its request. Using them correctly is fundamental to good API design:

Code Name When to Use
200 OK Successful GET, PUT, PATCH, or DELETE
201 Created Successful POST that creates a resource
204 No Content Successful DELETE with no response body
301 Moved Permanently Resource URL has permanently changed
304 Not Modified Cached version is still valid (ETag match)
400 Bad Request Malformed request syntax or invalid input
401 Unauthorized Authentication required or failed
403 Forbidden Authenticated but not authorized for this resource
404 Not Found Resource does not exist
409 Conflict Resource state conflict (duplicate email, etc.)
422 Unprocessable Entity Valid syntax but semantic errors (validation failed)
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unexpected server failure

A common mistake is returning 200 for everything and putting the real status in the response body. This breaks HTTP semantics, confuses intermediaries (caches, proxies, monitoring tools), and makes your API harder to consume. Use the correct status code, and use the response body for details.

REST Best Practices

Following established conventions makes your REST API predictable and easy to consume:

GraphQL: A Query Language for Your API

GraphQL was developed at Facebook in 2012 and open-sourced in 2015. It takes a fundamentally different approach from REST: instead of the server defining fixed endpoints that return fixed data shapes, the client specifies exactly what data it needs in a query language, and the server returns exactly that.

GraphQL Queries

A GraphQL query lets the client request specific fields from specific resources:

# A simple query — fetch a user with only the fields you need
query {
  user(id: 42) {
    name
    email
    role
  }
}

# Response — exactly the shape you requested
{
  "data": {
    "user": {
      "name": "Alice Chen",
      "email": "alice@example.com",
      "role": "SENIOR_DEVELOPER"
    }
  }
}

# Nested query — fetch related data in a single request
query {
  user(id: 42) {
    name
    posts(first: 5, orderBy: CREATED_AT_DESC) {
      title
      publishedAt
      comments {
        author { name }
        body
      }
    }
  }
}

In REST, getting the same data would require multiple requests: one for the user, one for their posts, and N more for comments on each post. GraphQL collapses this into a single round trip. This is particularly valuable for mobile clients on slow networks where every round trip adds latency.

Mutations

Mutations are GraphQL's equivalent of POST, PUT, PATCH, and DELETE in REST:

# Create a new user
mutation {
  createUser(input: {
    name: "Bob Martinez"
    email: "bob@example.com"
    role: DEVELOPER
  }) {
    id
    name
    email
    createdAt
  }
}

# Update a user
mutation {
  updateUser(id: 42, input: {
    role: SENIOR_DEVELOPER
  }) {
    id
    name
    role
    updatedAt
  }
}

# Delete a user
mutation {
  deleteUser(id: 42) {
    success
    message
  }
}

Subscriptions

GraphQL subscriptions provide real-time data through WebSocket connections:

# Subscribe to new comments on a post
subscription {
  commentAdded(postId: "post-123") {
    id
    body
    author {
      name
      avatar
    }
    createdAt
  }
}

Schema Design

The GraphQL schema is the contract between client and server. It defines types, queries, mutations, and subscriptions using the Schema Definition Language (SDL):

type User {
  id: ID!
  name: String!
  email: String!
  role: Role!
  posts: [Post!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  body: String!
  author: User!
  comments: [Comment!]!
  publishedAt: DateTime
  tags: [String!]!
}

enum Role {
  DEVELOPER
  SENIOR_DEVELOPER
  TEAM_LEAD
  ADMIN
}

type Query {
  user(id: ID!): User
  users(role: Role, limit: Int = 20, offset: Int = 0): [User!]!
  post(id: ID!): Post
  posts(authorId: ID, tag: String, limit: Int = 20): [Post!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): DeleteResult!
  createPost(input: CreatePostInput!): Post!
}

input CreateUserInput {
  name: String!
  email: String!
  role: Role!
}

input UpdateUserInput {
  name: String
  email: String
  role: Role
}

REST vs GraphQL: When to Use Which

Consideration REST GraphQL
Caching Built-in HTTP caching Requires custom caching (Apollo, Relay)
Over-fetching Common problem Clients request exactly what they need
Under-fetching Requires multiple requests Single request for related data
Learning curve Low (uses HTTP conventions) Moderate (new query language)
File uploads Native multipart support Requires extra spec (multipart request)
Error handling HTTP status codes Always 200, errors in response body
Versioning URL or header versioning Schema evolution (deprecate fields)
Best for Public APIs, simple CRUD, microservices Complex UIs, mobile apps, BFF pattern

Choose REST when you are building a public API, need simple caching, have straightforward CRUD operations, or want maximum compatibility. REST is also the better choice for machine-to-machine APIs where the data shape is predictable.

Choose GraphQL when your frontend needs vary significantly across pages or platforms, when you have deeply nested relational data, or when you want to reduce the number of round trips. GraphQL shines in applications like dashboards, social feeds, and e-commerce product pages.

API Authentication and Authorization

Authentication (who are you?) and authorization (what are you allowed to do?) are the most critical aspects of API security. Getting them wrong exposes your users' data and your infrastructure to attackers.

API Keys

The simplest authentication mechanism. The client includes a unique key with every request:

# API key in a header (preferred)
GET /api/v1/data
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0

# API key as a query parameter (less secure — logged in URLs)
GET /api/v1/data?api_key=sk_live_a1b2c3d4e5f6g7h8i9j0

API keys are best for server-to-server communication where you need to identify the calling application. They are not suitable for user authentication because they do not represent a specific user and cannot easily be scoped or revoked per-user. Always transmit API keys over HTTPS and never embed them in client-side code.

OAuth 2.0

OAuth 2.0 is the industry standard for delegated authorization. It allows users to grant third-party applications limited access to their resources without sharing their password. The most common flow for web applications is the Authorization Code flow:

# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
    response_type=code&
    client_id=YOUR_CLIENT_ID&
    redirect_uri=https://yourapp.com/callback&
    scope=read:users write:posts&
    state=random_csrf_token

# Step 2: User logs in and grants consent
# Authorization server redirects back to your app with a code:
# https://yourapp.com/callback?code=AUTH_CODE&state=random_csrf_token

# Step 3: Exchange the code for tokens (server-side)
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET

# Step 4: Use the access token for API calls
GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

OAuth 2.0 also supports other flows: the Client Credentials flow for machine-to-machine APIs (no user involved), the Device Code flow for smart TVs and CLI tools, and PKCE (Proof Key for Code Exchange) for mobile and single-page applications where the client secret cannot be kept confidential.

JSON Web Tokens (JWT)

JWTs are a compact, self-contained way to represent claims between parties. They are commonly used as OAuth 2.0 access tokens and for session management. A JWT has three parts separated by dots: header, payload, and signature.

# JWT structure: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiI0MiIsIm5hbWUiOiJBbGljZSBDaGVuIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzM4MjAwMDAwLCJleHAiOjE3MzgyMDM2MDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# Decoded header:
{
  "alg": "HS256",
  "typ": "JWT"
}

# Decoded payload:
{
  "sub": "42",
  "name": "Alice Chen",
  "role": "admin",
  "iat": 1738200000,
  "exp": 1738203600
}

# The signature verifies the token has not been tampered with
⚙ JWT Tools: Decode and inspect tokens with the JWT Decoder, or create test tokens with the JWT Generator. Both support HS256, RS256, and other common algorithms.

Key JWT best practices: always validate the signature server-side, check the exp (expiration) claim, use short-lived access tokens (15 minutes to 1 hour), pair them with longer-lived refresh tokens, and never store sensitive data in the payload (it is base64-encoded, not encrypted).

Basic Authentication

HTTP Basic Auth sends the username and password as a base64-encoded string in the Authorization header:

# Format: base64(username:password)
GET /api/v1/data
Authorization: Basic YWxpY2U6c2VjcmV0MTIz

# Decoded: alice:secret123

Basic Auth is simple but should only be used over HTTPS (credentials are not encrypted, just encoded) and is best suited for internal tools, scripts, and API testing. For production APIs serving external clients, prefer OAuth 2.0 or API keys.

API Design Best Practices

Good API design makes the difference between an API that developers love and one they dread. These practices are drawn from the APIs that developers consistently rate highest: Stripe, GitHub, Twilio, and Cloudflare.

Versioning

APIs evolve. Versioning lets you make breaking changes without breaking existing clients:

# URL path versioning (most common and explicit)
GET /api/v1/users
GET /api/v2/users

# Header versioning (cleaner URLs)
GET /api/users
Accept: application/vnd.myapi.v2+json

# Query parameter versioning
GET /api/users?version=2

URL path versioning is the most popular approach because it is explicit, easy to understand, and simple to route at the infrastructure level. The key rule: never break an existing version. Once v1 is published, v1 clients should keep working indefinitely (or at least with a generous deprecation timeline).

Pagination

Any endpoint that returns a list must support pagination. Returning thousands of records in a single response destroys performance for both the client and the server:

# Offset-based pagination (simple but has issues with large datasets)
GET /api/v1/posts?page=3&per_page=25

# Response includes pagination metadata
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 25,
    "total": 1247,
    "total_pages": 50
  }
}

# Cursor-based pagination (better for large, dynamic datasets)
GET /api/v1/posts?after=eyJpZCI6MTAwfQ&limit=25

# Response includes cursor for next page
{
  "data": [...],
  "pagination": {
    "has_next": true,
    "next_cursor": "eyJpZCI6MTI1fQ",
    "has_previous": true,
    "previous_cursor": "eyJpZCI6MTAxfQ"
  }
}

Offset pagination is simple to implement but suffers from two problems: performance degrades on large offsets (the database still scans all skipped rows), and results shift when items are inserted or deleted between page requests. Cursor-based pagination solves both problems and is the approach used by GitHub, Twitter, and Slack's APIs.

Filtering and Sorting

# Filtering with query parameters
GET /api/v1/products?category=electronics&price_min=100&price_max=500&in_stock=true

# Sorting
GET /api/v1/products?sort=price&order=asc
GET /api/v1/products?sort=-created_at  # Prefix with - for descending

# Field selection (sparse fieldsets)
GET /api/v1/users?fields=id,name,email

# Combining everything
GET /api/v1/products?category=electronics&price_min=100&sort=-rating&fields=id,name,price,rating&page=1&per_page=20

Error Handling

A consistent error response format is one of the most impactful things you can do for developer experience. Here is a pattern used by leading APIs:

# Consistent error response format
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request body contains invalid fields.",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address.",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "age",
        "message": "Must be between 13 and 150.",
        "code": "OUT_OF_RANGE"
      }
    ],
    "request_id": "req_a1b2c3d4",
    "documentation_url": "https://api.example.com/docs/errors#VALIDATION_ERROR"
  }
}

Every error response should include: a machine-readable error code (not just the HTTP status), a human-readable message, field-level details for validation errors, a request ID for support and debugging, and optionally a link to documentation explaining the error.

⚙ Debug it: Validate your API error responses with the JSON Validator and explore their structure in the JSON Viewer.

Rate Limiting

Rate limiting protects your API from abuse and ensures fair usage across all clients. The standard approach uses response headers to communicate limits:

# Response headers for rate limiting
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1738203600
Retry-After: 60

# When the limit is exceeded
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1738203600
Retry-After: 45

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded 1000 requests per hour. Please wait 45 seconds.",
    "retry_after": 45
  }
}

Common rate limiting strategies include fixed window (simple but allows bursts at window boundaries), sliding window (smoother but more complex), and token bucket (the most flexible, used by AWS and Cloudflare). Always return the Retry-After header so clients know when to retry.

HTTP Headers for APIs

HTTP headers carry critical metadata between clients and servers. Understanding the key headers for API communication is essential for both API consumers and designers.

Content-Type and Accept

# Content-Type tells the server what format the request body is in
POST /api/v1/users
Content-Type: application/json

{"name": "Alice", "email": "alice@example.com"}

# Accept tells the server what format you want the response in
GET /api/v1/users/42
Accept: application/json

# Content negotiation — server can return different formats
Accept: application/xml    # Request XML response
Accept: text/csv           # Request CSV export
Accept: application/json, application/xml;q=0.9  # Prefer JSON, accept XML

Authorization

# Bearer token (OAuth 2.0, JWT)
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

# Basic authentication
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

# API key (custom scheme)
Authorization: ApiKey sk_live_a1b2c3d4e5f6

# AWS Signature v4
Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260211/...

CORS (Cross-Origin Resource Sharing)

CORS headers allow APIs to be called from web browsers running on different domains. This is essential for any API consumed by frontend JavaScript:

# Preflight request (browser sends OPTIONS before the actual request)
OPTIONS /api/v1/users
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

# Server response to preflight
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

Avoid setting Access-Control-Allow-Origin: * if your API uses cookies or authorization headers. The wildcard origin does not work with credentialed requests and creates security risks. Instead, validate the origin against an allowlist and reflect it in the response.

⚙ Helpful tools: Encode URL parameters safely with the URL Encoder/Decoder, and inspect API response payloads with the JSON Viewer.

Other Important API Headers

# Idempotency key — ensures safe retries for non-idempotent operations
Idempotency-Key: unique-request-id-12345

# Request tracing — follows a request across microservices
X-Request-ID: req_a1b2c3d4
X-Correlation-ID: corr_e5f6g7h8

# Content encoding — compressed responses
Accept-Encoding: gzip, br
Content-Encoding: gzip

# Conditional requests — for efficient caching
If-None-Match: "etag-value-abc123"
If-Modified-Since: Wed, 11 Feb 2026 10:00:00 GMT

# ETag — response version identifier
ETag: "etag-value-abc123"
Last-Modified: Wed, 11 Feb 2026 09:30:00 GMT

API Testing and Debugging

Testing APIs thoroughly before and after deployment is non-negotiable. Bugs in APIs cascade to every client that depends on them.

Testing Tools

curl remains the most versatile command-line tool for API testing:

# Simple GET request
curl -s https://api.example.com/users/42 | jq .

# POST with JSON body
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"name": "Alice", "email": "alice@example.com"}' \
  | jq .

# See response headers
curl -i https://api.example.com/users/42

# Verbose mode — see the full request and response
curl -v https://api.example.com/users/42

# Measure response time
curl -o /dev/null -s -w "Total: %{time_total}s\nDNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\n" https://api.example.com/users

HTTPie offers a more human-friendly syntax:

# GET request (default)
http https://api.example.com/users/42

# POST with JSON (default content type)
http POST https://api.example.com/users name="Alice" email="alice@example.com"

# Custom headers
http https://api.example.com/users Authorization:"Bearer TOKEN"

# Form data
http --form POST https://api.example.com/upload file@photo.jpg

Automated API Testing

For comprehensive API testing, use frameworks that support contract testing, integration testing, and load testing:

# JavaScript — using Jest and supertest
const request = require('supertest');
const app = require('../app');

describe('GET /api/v1/users/:id', () => {
  it('returns a user by ID', async () => {
    const response = await request(app)
      .get('/api/v1/users/42')
      .set('Authorization', `Bearer ${token}`)
      .expect(200)
      .expect('Content-Type', /json/);

    expect(response.body).toHaveProperty('id', 42);
    expect(response.body).toHaveProperty('name');
    expect(response.body).toHaveProperty('email');
  });

  it('returns 404 for non-existent user', async () => {
    const response = await request(app)
      .get('/api/v1/users/99999')
      .set('Authorization', `Bearer ${token}`)
      .expect(404);

    expect(response.body.error.code).toBe('NOT_FOUND');
  });

  it('returns 401 without authentication', async () => {
    await request(app)
      .get('/api/v1/users/42')
      .expect(401);
  });
});
# Python — using pytest and requests
import pytest
import requests

BASE_URL = "https://api.example.com/v1"

class TestUsersAPI:
    def test_get_user(self, auth_headers):
        response = requests.get(f"{BASE_URL}/users/42", headers=auth_headers)
        assert response.status_code == 200
        data = response.json()
        assert "id" in data
        assert "name" in data
        assert "email" in data

    def test_create_user(self, auth_headers):
        payload = {"name": "Test User", "email": "test@example.com", "role": "developer"}
        response = requests.post(f"{BASE_URL}/users", json=payload, headers=auth_headers)
        assert response.status_code == 201
        assert response.json()["name"] == "Test User"

    def test_rate_limiting(self, auth_headers):
        for _ in range(1001):
            response = requests.get(f"{BASE_URL}/users", headers=auth_headers)
        assert response.status_code == 429
        assert "Retry-After" in response.headers

API Documentation: OpenAPI and Swagger

Great documentation is the difference between an API that gets adopted and one that gets abandoned. The OpenAPI Specification (formerly Swagger) is the industry standard for describing REST APIs.

OpenAPI 3.1 Example

openapi: 3.1.0
info:
  title: User Management API
  version: 1.0.0
  description: API for managing users and their resources
  contact:
    email: api-support@example.com

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging-api.example.com/v1
    description: Staging

paths:
  /users:
    get:
      summary: List all users
      operationId: listUsers
      tags: [Users]
      parameters:
        - name: role
          in: query
          schema:
            type: string
            enum: [developer, admin, viewer]
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: A paginated list of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /users/{id}:
    get:
      summary: Get a user by ID
      operationId: getUser
      tags: [Users]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: The user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum: [developer, admin, viewer]
        created_at:
          type: string
          format: date-time
      required: [id, name, email, role]

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            request_id:
              type: string

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

security:
  - bearerAuth: []

From an OpenAPI spec, you can automatically generate interactive documentation (Swagger UI, Redoc), client SDKs in dozens of languages (OpenAPI Generator), server stubs, and mock servers for frontend development. This makes the spec the single source of truth for your entire API ecosystem.

Documentation Best Practices

Modern API Protocols and Patterns

While REST and GraphQL dominate the API landscape, several other protocols and patterns are gaining traction for specific use cases in 2026.

gRPC

gRPC (Google Remote Procedure Call) uses Protocol Buffers for serialization and HTTP/2 for transport. It is significantly faster than REST for inter-service communication:

// user_service.proto — Protocol Buffer definition
syntax = "proto3";

package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc UpdateUser (UpdateUserRequest) returns (User);
  rpc DeleteUser (DeleteUserRequest) returns (DeleteResponse);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  Role role = 4;
  google.protobuf.Timestamp created_at = 5;
}

enum Role {
  ROLE_UNSPECIFIED = 0;
  DEVELOPER = 1;
  SENIOR_DEVELOPER = 2;
  ADMIN = 3;
}

message GetUserRequest {
  int32 id = 1;
}

message ListUsersRequest {
  Role role = 1;
  int32 page_size = 2;
  string page_token = 3;
}

gRPC advantages: binary serialization (5-10x smaller payloads than JSON), HTTP/2 multiplexing, bidirectional streaming, strong typing with code generation, and built-in deadline/timeout propagation. It is the standard for microservice communication at Google, Netflix, and Square. The tradeoff: no native browser support (requires grpc-web proxy) and less human-readable than JSON.

WebSockets

WebSockets provide full-duplex, persistent connections between client and server. They are essential for real-time applications:

// Client-side WebSocket connection
const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
  console.log('Connected');
  // Subscribe to events
  ws.send(JSON.stringify({
    type: 'subscribe',
    channels: ['trades', 'orderbook']
  }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  switch (data.type) {
    case 'trade':
      handleTrade(data.payload);
      break;
    case 'orderbook_update':
      handleOrderbook(data.payload);
      break;
  }
};

ws.onclose = (event) => {
  console.log(`Disconnected: ${event.code} ${event.reason}`);
  // Implement reconnection with exponential backoff
  setTimeout(() => reconnect(), getBackoffDelay());
};

Use WebSockets for: chat applications, live dashboards, financial data feeds, collaborative editing, multiplayer games, and any scenario where the server needs to push data to the client immediately. Do not use WebSockets when simple polling or Server-Sent Events would suffice, as WebSocket connections consume server resources and complicate horizontal scaling.

Server-Sent Events (SSE)

SSE provides a simpler alternative to WebSockets when you only need server-to-client streaming:

// Server-side (Node.js/Express)
app.get('/api/v1/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  const sendEvent = (data) => {
    res.write(`event: update\n`);
    res.write(`data: ${JSON.stringify(data)}\n`);
    res.write(`id: ${Date.now()}\n\n`);
  };

  // Send events as they occur
  const interval = setInterval(() => {
    sendEvent({ timestamp: new Date(), metric: Math.random() });
  }, 1000);

  req.on('close', () => clearInterval(interval));
});

// Client-side
const events = new EventSource('/api/v1/events');
events.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  updateDashboard(data);
});

SSE advantages over WebSockets: works over regular HTTP (no protocol upgrade), automatic reconnection built into the browser API, works with HTTP/2 multiplexing, simpler server implementation, and passes through proxies and firewalls without issues. SSE is the better choice for notifications, live feeds, log streaming, and progress updates.

tRPC

tRPC has emerged as a popular choice for TypeScript-heavy full-stack applications. It provides end-to-end type safety without code generation or schemas:

// Server — define your API router
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      const user = await db.users.findById(input.id);
      return user;
    }),

  createUser: t.procedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
      role: z.enum(['developer', 'admin']),
    }))
    .mutation(async ({ input }) => {
      return await db.users.create(input);
    }),
});

export type AppRouter = typeof appRouter;

// Client — full type safety, zero code generation
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '../server';

const client = createTRPCClient<AppRouter>({
  url: 'http://localhost:3000/trpc',
});

// TypeScript knows the exact return type
const user = await client.getUser.query({ id: 42 });
console.log(user.name); // Fully typed!

tRPC eliminates the API layer entirely for TypeScript monorepos. There is no OpenAPI spec to maintain, no code generation step, and no runtime overhead. The tradeoff is that it only works when both client and server are TypeScript, making it unsuitable for public APIs consumed by multiple languages.

API Security: The OWASP API Top 10

API security breaches have made headlines repeatedly in recent years. The OWASP API Security Top 10 identifies the most critical risks. Every API developer should understand and mitigate these threats.

1. Broken Object-Level Authorization (BOLA)

The number one API vulnerability. It occurs when an API does not verify that the authenticated user has permission to access the specific resource they are requesting:

# Attacker is user 42 but requests user 43's data
GET /api/v1/users/43/bank-accounts
Authorization: Bearer token_for_user_42

# Vulnerable API returns user 43's data without checking ownership

# Fix: Always verify resource ownership
async function getBankAccounts(req, res) {
  const accounts = await db.bankAccounts.find({ userId: req.params.userId });

  // CRITICAL: Verify the authenticated user owns this resource
  if (req.params.userId !== req.auth.userId && !req.auth.isAdmin) {
    return res.status(403).json({
      error: { code: "FORBIDDEN", message: "You cannot access another user's accounts." }
    });
  }

  return res.json({ data: accounts });
}

2. Broken Authentication

Weak authentication mechanisms allow attackers to impersonate legitimate users. Common flaws include: accepting weak passwords, not implementing rate limiting on login endpoints, exposing tokens in URLs, not validating JWT signatures, and using predictable API keys.

3. Broken Object Property-Level Authorization

The API exposes object properties that the user should not be able to read or write:

# User submits a profile update but includes an admin field
PATCH /api/v1/users/42
{
  "name": "Alice Chen",
  "role": "admin"  // Should not be settable by the user!
}

# Fix: Explicitly whitelist allowed fields
const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'avatar', 'bio'];

function sanitizeInput(body) {
  return Object.fromEntries(
    Object.entries(body).filter(([key]) => ALLOWED_UPDATE_FIELDS.includes(key))
  );
}

4. Unrestricted Resource Consumption

APIs that do not limit request rates, payload sizes, or query complexity are vulnerable to denial-of-service attacks:

# GraphQL depth attack — nested queries that explode into millions of database calls
query {
  user(id: 1) {
    friends {
      friends {
        friends {
          friends {
            friends { name }
          }
        }
      }
    }
  }
}

# Mitigations:
# - Rate limiting (requests per minute per client)
# - Query depth limiting (max 5 levels for GraphQL)
# - Query cost analysis (assign costs to fields)
# - Payload size limits (max 1MB request body)
# - Pagination enforcement (max 100 items per page)
# - Timeout limits (kill long-running queries)

5. Additional Security Best Practices

Putting It All Together: API Design Checklist

Before shipping any API, run through this checklist. It distills the practices covered in this guide into actionable items:

  1. Resource Design: Resources use plural nouns. URIs are clean and hierarchical. Relationships are modeled through nesting (max 2 levels).
  2. HTTP Methods: Each endpoint uses the correct method (GET for reads, POST for creates, etc.). Idempotency is guaranteed where expected (PUT, DELETE).
  3. Status Codes: Responses use semantically correct HTTP status codes. Errors include machine-readable codes and human-readable messages.
  4. Authentication: All endpoints require authentication (except health checks and documentation). Tokens are short-lived. Sensitive endpoints have additional authorization checks.
  5. Pagination: All list endpoints support pagination. Default page sizes are reasonable (20-50). Maximum page sizes are enforced.
  6. Versioning: The API is versioned from day one. The versioning strategy is consistent across all endpoints.
  7. Rate Limiting: All endpoints are rate-limited. Limits are communicated via response headers. 429 responses include Retry-After.
  8. Validation: All input is validated. Error messages identify which fields failed and why.
  9. Documentation: An OpenAPI spec exists and is kept in sync with the implementation. Interactive documentation is available.
  10. Security: HTTPS only. CORS is configured correctly. Input is sanitized. No sensitive data in logs or error messages. BOLA checks on every endpoint.
  11. Testing: Integration tests cover happy paths, error paths, authentication, and edge cases. Load tests verify performance under expected traffic.

Conclusion

Building great APIs in 2026 means understanding the full spectrum of tools and patterns available to you. REST remains the foundation, battle-tested and universally supported. GraphQL solves real problems with data fetching in complex frontends. gRPC dominates high-performance service-to-service communication. WebSockets and SSE enable real-time features. tRPC eliminates the API boundary in TypeScript monorepos.

No matter which protocol or pattern you choose, the fundamentals remain the same: design clear, consistent interfaces; authenticate and authorize every request; validate all input; handle errors gracefully; document everything; and test thoroughly. The APIs you build are the contracts other developers rely on. Make them reliable, predictable, and a pleasure to use.

Start small. Pick one area from this guide, whether it is improving your error handling, adding rate limiting, or writing an OpenAPI spec for your existing API, and implement it this week. Good API design is not a one-time effort. It is a practice you refine continuously as your understanding deepens and your users' needs evolve.

Related Tools and Resources

HTTP Request Tester
Send GET, POST, PUT, DELETE requests and inspect responses
JSON Formatter
Format, validate, and beautify JSON API responses
JSON Validator
Validate JSON structure and syntax
JSON Viewer
Explore and navigate JSON data visually
JWT Decoder
Decode and inspect JSON Web Tokens
JWT Generator
Create signed JWT tokens for testing
URL Encoder/Decoder
Encode and decode URL parameters safely