Styling Guides

API Design Guidelines

Principles for designing clean, predictable, and versioned interfaces.

Overview

API design is the practice of defining interfaces that are intuitive to use, hard to misuse, and stable over time. Principles: make the common case simple and the complex case possible, fail fast with clear error messages, use consistent patterns across all endpoints, version explicitly, and design for the consumer not the implementation. REST, GraphQL, and gRPC are the dominant paradigms.

Origin

Roy Fielding defined REST in his 2000 PhD dissertation, extracting the architectural constraints of the web. Leonard Richardson's Maturity Model (2008) graded REST implementations 0-3. GraphQL was developed at Facebook (2012, open-sourced 2015) to solve over-fetching and under-fetching in mobile APIs. gRPC (Google, 2015) built on Protocol Buffers for high-performance service-to-service communication.

Examples

RESTful resource design with consistent error shape in TypeScript

import { Router, Request, Response, NextFunction } from 'express';

interface ApiError {
  error: {
    code: string;
    message: string;
    details?: Record<string, string[]>;
  };
}

function apiError(res: Response, status: number, code: string, message: string): void {
  const body: ApiError = { error: { code, message } };
  res.status(status).json(body);
}

const router = Router();

// GET /api/v1/orders/:id
router.get('/api/v1/orders/:id', async (req: Request, res: Response) => {
  const { id } = req.params;
  const order = await OrderRepository.findById(id);
  if (!order) return apiError(res, 404, 'ORDER_NOT_FOUND', 'No order with id ' + id);
  res.json({ data: order });
});

// POST /api/v1/orders
router.post('/api/v1/orders', async (req: Request, res: Response) => {
  const result = CreateOrderSchema.safeParse(req.body);
  if (!result.success) {
    return apiError(res, 422, 'VALIDATION_ERROR', 'Invalid request body');
  }
  const order = await OrderService.create(result.data);
  res.status(201).json({ data: order });
});

Consistent error shape (error.code, error.message) allows clients to handle errors programmatically without parsing message strings. apiError centralises status codes and prevents ad-hoc error responses scattered across handlers.

Versioning and deprecation headers in Ruby/Rack

# API versioning via URL prefix + deprecation response headers
class ApiVersionMiddleware
  DEPRECATED_VERSIONS = ['v1'].freeze
  SUNSET_DATES = { 'v1' => Date.new(2026, 6, 1) }.freeze

  def initialize(app)
    @app = app
  end

  def call(env)
    version = extract_version(env['PATH_INFO'])
    status, headers, body = @app.call(env)

    if DEPRECATED_VERSIONS.include?(version)
      headers['Deprecation'] = 'true'
      headers['Sunset'] = SUNSET_DATES[version].httpdate
      headers['Link'] = '</api/v2>; rel="successor-version"'
    end

    [status, headers, body]
  end

  private

  def extract_version(path)
    path.match(%r{/api/(vd+)/})&.captures&.first
  end
end

The Deprecation and Sunset headers follow IETF draft (draft-ietf-httpapi-deprecation-header-02). Clients that observe these headers can surface deprecation warnings in their monitoring dashboards before the sunset date.

Use Cases

  • 01Public APIs consumed by third-party developers where breaking changes require versioning and a deprecation timeline
  • 02Mobile app backends where over-fetching wastes bandwidth on constrained networks; GraphQL or field selection mitigates this
  • 03Internal microservice APIs where a protobuf schema (gRPC) enforces a machine-verified contract between services
  • 04Frontend-backend teams where the API contract (OpenAPI schema) is agreed and code-generated on both sides to prevent drift

When Not to Use

  • //Do not use REST for server-to-server streaming of large binary data; gRPC bidirectional streaming or HTTP chunked transfer is more appropriate
  • //Do not design CRUD-shaped REST APIs for complex business operations that span multiple resources; a command endpoint (POST /orders/:id/approve) is clearer than PATCH /orders/:id with status in the body
  • //Do not version via query parameters (?version=2) or Accept headers in simple projects; URL versioning (/v2/) is more explicit, cache-friendly, and easier to route

Technical Notes

  • OpenAPI 3.1 (2021) aligns with JSON Schema 2020-12, enabling spec-first development where types are generated for both client and server via tools like openapi-generator and openapi-typescript
  • Idempotency keys (Stripe pattern) prevent double-processing on retried POST requests; the server stores the result keyed by the client-provided Idempotency-Key header and returns the cached response on retry
  • Cursor-based pagination (after: "cursor-xyz") is superior to offset-based pagination for large or frequently-updated datasets; offset-based pagination produces duplicate or skipped items when rows are inserted between requests
  • HTTP 422 Unprocessable Entity (not 400 Bad Request) is the correct status for validation errors where the request syntax is valid but the content violates business rules; RFC 9110 clarifies this distinction