Low Level Design: API Documentation Service

Overview

An API documentation service ingests API specifications (OpenAPI/Swagger, GraphQL schemas, gRPC protobufs), renders human-readable documentation, hosts an interactive sandbox where developers can execute live API calls, tracks versions and changelogs, and integrates into a developer portal. This is a common senior-level system design question because it touches spec parsing, content storage, real-time proxying, search indexing, and developer experience at scale.

Core Components

  1. Spec Ingestion Pipeline: Accepts OpenAPI 3.x, Swagger 2.0, GraphQL SDL, protobuf definitions. Validates, normalizes to an internal representation, and stores.
  2. Version Registry: Tracks spec versions per API, diffs between versions, and generates structured changelogs.
  3. Documentation Renderer: Transforms the internal spec representation into HTML/React components for the developer portal.
  4. Live Sandbox (Try-It-Out): Proxies authenticated requests from the browser to the actual API, handling CORS, credential injection, and response display.
  5. Search Index: Full-text search across all endpoints, schemas, and descriptions.
  6. Developer Portal: Web application serving the rendered documentation, sandbox, changelogs, and SDKs.

Data Model

-- API registrations
CREATE TABLE apis (
    api_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    name            VARCHAR(255) NOT NULL,
    slug            VARCHAR(255) NOT NULL,
    spec_format     VARCHAR(20) NOT NULL DEFAULT 'openapi3', -- openapi3, swagger2, graphql, protobuf
    base_url        VARCHAR(1000),                           -- production base URL
    sandbox_url     VARCHAR(1000),                           -- sandbox/staging base URL
    visibility      VARCHAR(20) NOT NULL DEFAULT 'private', -- private, internal, public
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(tenant_id, slug)
);

-- API spec versions
CREATE TABLE api_versions (
    version_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    api_id          UUID NOT NULL REFERENCES apis(api_id) ON DELETE CASCADE,
    version_label   VARCHAR(100) NOT NULL,          -- semver or date-based label
    spec_raw        TEXT NOT NULL,                  -- original spec text (YAML/JSON/SDL/proto)
    spec_normalized JSONB NOT NULL,                 -- parsed internal representation
    spec_hash       CHAR(64) NOT NULL,              -- SHA-256 of spec_raw for dedup
    is_current      BOOLEAN NOT NULL DEFAULT false,
    published_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    published_by    UUID,
    UNIQUE(api_id, version_label)
);

CREATE INDEX idx_api_versions_api ON api_versions(api_id, published_at DESC);

-- Changelog entries (generated by diffing consecutive versions)
CREATE TABLE api_changelogs (
    changelog_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    api_id          UUID NOT NULL REFERENCES apis(api_id) ON DELETE CASCADE,
    from_version    UUID REFERENCES api_versions(version_id),
    to_version      UUID NOT NULL REFERENCES api_versions(version_id),
    change_type     VARCHAR(20) NOT NULL,   -- BREAKING, DEPRECATED, ADDED, MODIFIED, REMOVED
    path            VARCHAR(500),           -- affected endpoint or schema path
    description     TEXT NOT NULL,
    generated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_changelog_api ON api_changelogs(api_id, generated_at DESC);

-- Endpoints extracted from specs (for fast search and listing)
CREATE TABLE api_endpoints (
    endpoint_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    version_id      UUID NOT NULL REFERENCES api_versions(version_id) ON DELETE CASCADE,
    api_id          UUID NOT NULL,
    method          VARCHAR(10) NOT NULL,   -- GET, POST, PUT, DELETE, PATCH, etc.
    path            VARCHAR(1000) NOT NULL,
    summary         VARCHAR(500),
    description     TEXT,
    tags            TEXT[],
    deprecated      BOOLEAN NOT NULL DEFAULT false,
    search_vector   TSVECTOR GENERATED ALWAYS AS (
        to_tsvector('english', coalesce(path,'') || ' ' || coalesce(summary,'') || ' ' || coalesce(description,''))
    ) STORED
);

CREATE INDEX idx_endpoints_version ON api_endpoints(version_id);
CREATE INDEX idx_endpoints_search ON api_endpoints USING GIN(search_vector);

-- Sandbox credentials per user (encrypted API keys for try-it-out)
CREATE TABLE sandbox_credentials (
    cred_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    api_id          UUID NOT NULL REFERENCES apis(api_id) ON DELETE CASCADE,
    user_id         UUID NOT NULL,
    cred_type       VARCHAR(30) NOT NULL,   -- API_KEY, BEARER, BASIC, OAUTH2
    encrypted_value BYTEA NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at      TIMESTAMPTZ,
    UNIQUE(api_id, user_id, cred_type)
);

Spec Ingestion Workflow

  1. Upload: Client POSTs the spec file (or a URL to pull from). Max size limit (e.g., 5 MB). Stored to S3/GCS raw, keyed by SHA-256 hash for deduplication.
  2. Parse and Validate: A worker pulls from the ingestion queue. Uses spec-specific parsers:
    • OpenAPI: kin-openapi (Go) or swagger-parser (Node). Validate against the JSON Schema for OpenAPI 3.0.
    • GraphQL SDL: graphql-js schema builder with validation.
    • Protobuf: protoc with descriptor extraction.

    Return structured errors (line number, field name, message) on validation failure.

  3. Normalize: Convert parsed spec to an internal canonical JSON format. All spec formats are represented uniformly: a list of endpoints with their request/response schemas, authentication requirements, and metadata. This decouples the renderer and search indexer from spec-format-specific logic.
  4. Diff: Compare the new normalized spec against the previous current version using a recursive JSON diff algorithm. Categorize changes:
    • BREAKING: removed endpoint, changed required parameter type, removed required response field
    • DEPRECATED: endpoint or parameter marked deprecated
    • ADDED: new endpoint or optional parameter
    • MODIFIED: changed description, example, or optional field type
  5. Persist: Write api_versions row, api_endpoints rows, api_changelogs rows in a transaction. Flip is_current = false on the previous version.
  6. Invalidate cache: Publish a cache invalidation event for the affected api_id. CDN edges purge rendered documentation pages.

Changelog Generation Algorithm

Diff algorithm operates on the normalized spec tree. Core steps:

function diff_specs(old_spec, new_spec):
    changes = []

    // Endpoint-level diff
    old_endpoints = index_by(old_spec.endpoints, [method, path])
    new_endpoints = index_by(new_spec.endpoints, [method, path])

    for key in old_endpoints - new_endpoints:
        changes.append(BREAKING, key, "Endpoint removed")

    for key in new_endpoints - old_endpoints:
        changes.append(ADDED, key, "Endpoint added")

    for key in old_endpoints intersect new_endpoints:
        old_ep = old_endpoints[key]
        new_ep = new_endpoints[key]

        // Parameter diff
        for param in old_ep.required_params - new_ep.required_params:
            changes.append(BREAKING, key, "Required parameter removed: " + param)
        for param in new_ep.required_params - old_ep.required_params:
            changes.append(BREAKING, key, "New required parameter: " + param)

        // Schema diff (recursive)
        schema_changes = recursive_schema_diff(old_ep.response_schema, new_ep.response_schema)
        changes.extend(schema_changes)

        if new_ep.deprecated and not old_ep.deprecated:
            changes.append(DEPRECATED, key, "Endpoint deprecated")

    return changes

Live Sandbox Architecture

The Try-It-Out feature lets developers fill in parameter values in the browser and execute real API calls. Security and reliability requirements make this non-trivial:

Proxy Architecture

Browsers cannot directly call most APIs due to CORS restrictions. The sandbox service acts as a proxy:

  1. Developer submits a request form: method, path, headers, body, credential selection.
  2. Browser sends the request to the sandbox proxy endpoint: POST /sandbox/proxy with the target API identified by api_id.
  3. Proxy validates: user is authorized to call this API, rate limit not exceeded, target URL is on the allowlist for this api_id.
  4. Proxy injects the user’s stored sandbox credentials (decrypted from sandbox_credentials table).
  5. Proxy forwards the request to the API’s sandbox_url with a timeout (default 30s).
  6. Proxy returns the response (status, headers, body) to the browser.

Security Controls

  • URL allowlist: Only forward to pre-registered sandbox_url values. Never allow arbitrary URL injection (SSRF prevention).
  • Credential isolation: Sandbox credentials are encrypted at rest. Decrypted in memory only for the duration of the proxied request. Never returned to the browser.
  • Rate limiting: Per-user, per-API rate limit on sandbox calls to prevent abuse of upstream APIs.
  • Response sanitization: Strip Set-Cookie headers and other sensitive response headers before returning to the browser. Optionally truncate very large response bodies.

Search Index

Full-text search across all endpoints, schema fields, and descriptions:

  • Online (PostgreSQL FTS): For small to medium deployments, the search_vector GIN index on api_endpoints is sufficient. Query with WHERE search_vector @@ plainto_tsquery('english', input).
  • Offline (Elasticsearch/OpenSearch): For large-scale developer portals (millions of endpoints across thousands of APIs), push normalized endpoint data to a search cluster. Index per tenant with separate field mappings. Supports fuzzy matching, synonym expansion (GET → retrieve, fetch), and faceted filtering by tag, method, and deprecation status.
  • Index freshness: After ingestion, emit an event to a search indexer worker. Worker upserts endpoint documents. Stale documents (from replaced versions) are deleted by version_id.

Version Tracking and Rollback

  • All versions are stored permanently (unless explicitly deleted by tenant). The current version is flagged; all others are accessible by version label.
  • Rollback: mark a previous version as current, regenerate the diff between the new current and the version it superseded, and emit a new changelog entry noting the rollback.
  • Deprecation policy: tenants can configure automatic sunset dates per version. A background job sends notifications and, after the sunset date, returns 410 Gone on docs requests for that version.

Failure Handling and Edge Cases

  • Invalid spec: Return detailed validation errors with line numbers and field paths. Do not partially persist a failed spec. Return the full error list (not just the first error) so the developer can fix all issues in one edit cycle.
  • Spec too large: Reject at upload time with a 413 and a clear message. For legitimate large specs, offer a multi-part upload or a URL-pull mode (service fetches from a URL asynchronously).
  • Sandbox timeout: Return a timeout error with the elapsed time. Do not hang the browser connection. Use async polling if latency is expected to exceed 30 seconds.
  • Circular references in spec: OpenAPI schemas can have circular chains. The parser must detect cycles (via DFS with a visited set) and represent them safely in the normalized format (e.g., break cycles with a pointer in the internal format).
  • Concurrent version uploads: Use an optimistic lock on is_current: UPDATE api_versions SET is_current = false WHERE api_id = ? AND is_current = true returning version_id, then INSERT the new version. If another upload races and flips is_current first, detect the conflict and retry or reject with a 409.
  • CDN cache staleness: Documentation pages are cached at the CDN edge for performance. On version publish, send purge requests to the CDN API for affected URLs (the current docs URL and the versioned URL). Use surrogate keys (cache tags) for efficient targeted purge.

Scalability Considerations

  • Rendering: Documentation HTML can be rendered at publish time (static site generation) rather than on each request. Store rendered HTML in S3 and serve via CDN. Only re-render on spec update. Dynamic elements (sandbox, search) load separately as client-side components.
  • Spec storage: Raw spec text is stored in object storage (S3), not in the database. The database stores the normalized JSON and metadata. This keeps database row sizes manageable and allows efficient streaming of large specs.
  • Ingestion queue: Use a work queue (SQS, Kafka, or Pub/Sub) between the upload API and the parsing workers. This decouples upload latency from parsing time (which can take several seconds for large specs with deep schema resolution).
  • Multi-tenant search: Partition the search index by tenant_id to ensure search isolation and allow per-tenant scaling. Public APIs across tenants can be included in a global public index for discoverability.
  • Sandbox proxy scaling: Proxy workers are stateless and I/O-bound (waiting on upstream API responses). Scale horizontally. Use async I/O (Node.js, Go goroutines) for high concurrency with low memory overhead per request.

Summary

Designing an API documentation service requires combining a reliable spec ingestion pipeline with a robust version diffing engine, a secure live sandbox proxy, full-text search across endpoint metadata, and CDN-backed static rendering for performance. The key interview insights are: normalize all spec formats to a single internal representation early in the pipeline to keep downstream components simple; generate changelogs by semantic diff (not text diff) to produce meaningful, categorized change entries; prevent SSRF in the sandbox proxy through strict URL allowlisting and credential isolation; and favor static generation with targeted CDN invalidation over dynamic rendering to handle high traffic on popular public APIs without database pressure per page view.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is an API documentation service and what are its core components?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “An API documentation service is a platform that ingests API specifications (typically OpenAPI/Swagger, AsyncAPI, or GraphQL SDL), renders them as human-readable reference pages, and provides interactive tooling for developers. Its core components are: (1) a spec ingestion and storage layer that accepts uploads or pulls specs from source control; (2) a rendering engine that converts spec definitions into structured HTML documentation with endpoint descriptions, parameter tables, and response schemas; (3) a versioning system that stores historical spec snapshots and diffs between them; (4) an interactive sandbox or ‘try it out’ console that lets developers make live API calls; and (5) a search index over all endpoints, schemas, and descriptions to support fast lookup.”
}
},
{
“@type”: “Question”,
“name”: “How does an API documentation service generate changelogs between API versions?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The service stores every uploaded OpenAPI spec as an immutable snapshot keyed by version identifier. When a new version is published, a diff engine compares the two spec objects structurally: it walks paths, operations, parameters, request bodies, and response schemas, classifying each change as added, removed, or modified. Changes are further classified by severity — breaking (removed endpoints, required parameter added, response field removed) vs. non-breaking (new optional parameter, new response field, description update). The diff is serialized into a structured changelog entry (JSON or Markdown) and stored alongside the spec snapshot. The documentation portal renders this as a human-readable changelog page, with breaking changes highlighted so API consumers can assess upgrade impact.”
}
},
{
“@type”: “Question”,
“name”: “How is a live sandbox implemented in an API documentation portal?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A live sandbox is implemented by embedding an HTTP client in the documentation UI that constructs and sends real requests to a dedicated sandbox environment. The portal pre-populates request forms from the OpenAPI spec (path parameters, query parameters, request body schema). The developer fills in values and clicks ‘Send’; the browser calls a portal-side proxy that appends a sandbox API key, forwards the request to the sandbox backend, and streams the response back to the UI. The sandbox backend runs the same code as production but is pointed at isolated, resettable data stores seeded with synthetic data. Rate limits are applied per developer account to prevent abuse. Some portals also offer a mock server mode that returns spec-defined example responses without hitting a real backend at all.”
}
},
{
“@type”: “Question”,
“name”: “How does an API documentation service handle versioning of OpenAPI specs?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The service treats each OpenAPI spec upload as an immutable artifact identified by a semantic version (e.g., v1.3.2) or a commit SHA. Specs are stored in object storage (S3-compatible) with a metadata record in a relational database that tracks version, upload timestamp, uploader, and status (draft, published, deprecated). The portal exposes a version selector that lets users switch between published versions; each version renders its own documentation in isolation. A ‘latest’ pointer is maintained separately and updated only when a version is explicitly promoted. The diff engine (described above) operates on any two stored snapshots. Deprecation warnings are shown on old versions, with a configurable sunset date after which the version is hidden from the selector but still accessible via a direct URL for existing integrators.”
}
}
]
}

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Atlassian Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top