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
- Spec Ingestion Pipeline: Accepts OpenAPI 3.x, Swagger 2.0, GraphQL SDL, protobuf definitions. Validates, normalizes to an internal representation, and stores.
- Version Registry: Tracks spec versions per API, diffs between versions, and generates structured changelogs.
- Documentation Renderer: Transforms the internal spec representation into HTML/React components for the developer portal.
- Live Sandbox (Try-It-Out): Proxies authenticated requests from the browser to the actual API, handling CORS, credential injection, and response display.
- Search Index: Full-text search across all endpoints, schemas, and descriptions.
- 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
- 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.
- Parse and Validate: A worker pulls from the ingestion queue. Uses spec-specific parsers:
- OpenAPI:
kin-openapi(Go) orswagger-parser(Node). Validate against the JSON Schema for OpenAPI 3.0. - GraphQL SDL:
graphql-jsschema builder with validation. - Protobuf:
protocwith descriptor extraction.
Return structured errors (line number, field name, message) on validation failure.
- OpenAPI:
- 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.
- 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
- Persist: Write
api_versionsrow,api_endpointsrows,api_changelogsrows in a transaction. Flipis_current = falseon the previous version. - 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:
- Developer submits a request form: method, path, headers, body, credential selection.
- Browser sends the request to the sandbox proxy endpoint:
POST /sandbox/proxywith the target API identified byapi_id. - Proxy validates: user is authorized to call this API, rate limit not exceeded, target URL is on the allowlist for this
api_id. - Proxy injects the user’s stored sandbox credentials (decrypted from
sandbox_credentialstable). - Proxy forwards the request to the API’s
sandbox_urlwith a timeout (default 30s). - Proxy returns the response (status, headers, body) to the browser.
Security Controls
- URL allowlist: Only forward to pre-registered
sandbox_urlvalues. 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_vectorGIN index onapi_endpointsis sufficient. Query withWHERE 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 apointer 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 flipsis_currentfirst, 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_idto 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