API Documentation and Developer Experience Questions
Documentation and developer experience for API consumers: reference docs for endpoints and fields, OpenAPI-driven and interactive documentation, quickstarts and runnable code samples, doc comments that feed generated reference, changelogs, deprecation notices and migration guides, error messages and error tables that explain themselves, developer portals, sandboxes and onboarding flows. Covers developer experience (DX) as a product concern: time to first successful call, DX and docs-effectiveness metrics and dashboards, diagnosing onboarding drop-off, developer feedback loops, experiments and developer research, documentation standards and CI checks across teams, prioritising DX investment, and reducing support load through better docs and self-service. Designing the API itself (versioning policy, auth, rate limits, webhooks, gateways, SDK engineering) is a separate subject.
You are asked to write the doc comments for a new SDK method so they serve both engineers reading the code and the automated reference docs generated from them. What practices do you apply? Show a short example for a tax-calculation method.
Sample Answer
Direct answer
Write the comment for two readers at once: the engineer who sees it in the editor tooltip, and the documentation generator that turns it into a reference page. Use the language's standard doc-comment format (a docstring in Python, Javadoc in Java, TSDoc in TypeScript: comment conventions that documentation tools parse), lead with a one-line summary, document every parameter with units and limits, and include an example that is tested so it cannot go stale. Only the Python form is needed for the example below; the Java and TypeScript names are just the same idea in other languages, so do not memorise the tools. (An SDK, software development kit, is the language-specific library that wraps an API.) This is "docs-as-code": the documentation lives next to the code and is reviewed and built with it.
Practices to apply
- Summary line first, in the imperative (a command form: "Calculate the sales tax...", not "Calculates" or "This function calculates"). Generators use it as the list description.
- Say what, and the contract: units, allowed ranges, rounding, side effects (here: no network call, no mutation).
- Document each parameter and return value, including the type and what the number means (currency units, not just "a number").
- List the errors raised, and under which conditions.
- Include a runnable example. In Python, examples written as
>>>lines can be executed by the standarddoctestmodule, so the docs fail the build when the behaviour changes. - Do not repeat the function name or narrate the implementation; comments explain what a caller needs.
- Mention versioning notes when behaviour changes.
Example: a tax-calculation method (Python)
from decimal import Decimal, ROUND_HALF_UP
RATES = {"US-CA": Decimal("0.0725"), "US-OR": Decimal("0")}
def calculate_tax(amount, jurisdiction, *, tax_exempt=False):
"""Calculate the sales tax owed on an order subtotal.
Looks up the rate for ``jurisdiction`` and rounds the result to whole
cents (half up). Makes no network call and never changes the order.
Args:
amount (Decimal): Taxable subtotal in the order currency, for example
``Decimal("100.00")``. Must not be negative. Shipping is only
taxed if the caller includes it in this amount.
jurisdiction (str): Region code such as ``"US-CA"``.
tax_exempt (bool): If True, returns zero without a rate lookup.
Defaults to False.
Returns:
Decimal: The tax amount, rounded to 2 decimal places.
Raises:
ValueError: If ``amount`` is negative or ``jurisdiction`` has no rate.
Example:
>>> calculate_tax(Decimal("100.00"), "US-CA")
Decimal('7.25')
>>> calculate_tax(Decimal("19.99"), "US-CA")
Decimal('1.45')
"""
if amount < 0:
raise ValueError("amount must not be negative")
if tax_exempt:
return Decimal("0.00")
if jurisdiction not in RATES:
raise ValueError(f"no tax rate for jurisdiction {jurisdiction!r}")
return (amount * RATES[jurisdiction]).quantize(Decimal("0.01"), ROUND_HALF_UP)
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Running this file prints TestResults(failed=0, attempted=2): both examples in the docstring were executed and matched.
Two code details in plain words: the bare * in the signature makes everything after it keyword-only, so callers must write tax_exempt=True and cannot pass a bare True by position (which would be unreadable at the call site). quantize(Decimal("0.01"), ROUND_HALF_UP) rounds to two decimal places, and ROUND_HALF_UP means an exact half rounds up (1.005 becomes 1.01), the rule most tax authorities expect.
Why this serves both readers
The docstring's Args/Returns/Raises sections are parsed by a generator such as Sphinx (a Python documentation generator, with its autodoc extension that reads docstrings; mkdocstrings is a similar alternative) into a reference page, while the same text appears in the editor when an engineer hovers over the function. Worked check by hand: 19.99 x 0.0725 = 1.449275, which rounds half up to 1.45, matching the second example.
Pitfalls
- Example output copied by hand rather than executed goes stale silently.
- Money as floating point causes rounding surprises; the example uses
Decimal, and the docs say so. - Undocumented rounding rules are the most common source of "your tax is one cent off" support tickets.
How would you structure and maintain an OpenAPI specification so it produces both readable reference docs and usable client code? Focus on what goes in the spec beyond the schemas, and how you keep it from drifting away from the real API.
Sample Answer
Direct answer
OpenAPI is a machine-readable description format for HTTP APIs (a YAML or JSON file). Treat the spec as a product with two audiences: humans reading rendered reference docs, and code generators producing SDKs (software development kits, client libraries in a given language). Schemas alone give you neither: readable docs need descriptions, examples and errors, and usable clients need stable operation names, auth and servers. Drift means the file slowly stops matching what the running API really does. Keep it from drifting by making CI (continuous integration, the automated checks on every pull request) fail when the spec and the running API disagree.
What goes in the spec beyond schemas
operationIdon every operation. Unique and stable. Generators typically turn it into the SDK method name, so renaming it is a breaking change for SDK users.summary,descriptionandtags. These become the docs page text and navigation groups.exampleson requests and responses. They render as sample payloads and drive mock servers (fake API servers that answer from the examples, so front-end developers can build before the real service exists).- Every error response, not just
200, referencing one shared error schema. serversandsecuritySchemes. Base URLs per environment, and how auth works (for example a bearer token sent in theAuthorizationheader), so clients configure themselves.- Reusable
components(parameters, responses, schemas) referenced with$ref(a pointer that means "use the definition stored over there"), so pagination or error shapes are defined once. deprecated: trueon retiring operations, plus vendor extensions (fields startingx-, such asx-codeSamples, an optional extension some doc renderers use to show code samples) for extras.- Split a big spec across files and bundle it (merge the pieces back into one file) for publishing.
paths:
/invoices:
post:
operationId: createInvoice
summary: Create an invoice
description: Creates a draft invoice for an existing customer.
tags: [Invoices]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customer_id, amount_cents]
properties:
customer_id: {type: string, example: cus_123}
amount_cents: {type: integer, minimum: 1, example: 2500}
responses:
"201": {description: Created}
"422":
description: Validation failed
content:
application/json:
schema: {$ref: "#/components/schemas/Error"}
components:
schemas:
Error:
type: object
required: [code, message]
properties:
code: {type: string}
message: {type: string}
Reading it line by line: operationId: createInvoice becomes the SDK method name (typically client.invoices.create(...)); summary is the one-line title on the docs page and description is its body text; tags: [Invoices] decides which navigation group and which SDK class the operation lands in; the example values are the sample payload shown in the docs; and the 422 response, pointing at the shared Error schema through $ref, becomes both the documented error body and the error type the SDK raises.
Keeping it from drifting
The standard core set is a linter, a breaking-change diff and a contract test; the rest is optional polish.
- Lint the spec (a linter is a tool that checks a file against style rules) with Spectral, the common OpenAPI linter: the built-in
spectral:oasruleset only WARNS on a missing operationdescriptionoroperationId(executed: Spectral run on the shipped spec with both removed printedoperation-descriptionandoperation-operationIdas warnings, 0 errors), and it does not flag a missing example at all, because its example rule only validates examples that exist. So write a custom ruleset that raises those rules toerrorand adds a rule requiring examples, then make CI fail on errors. - Detect breaking changes (edits that make existing clients stop working, such as removing a field or renaming an operation): diff the spec against the main branch (for example with oasdiff, a tool that compares two versions of a spec and reports what changed) and block removed fields or renamed operations without an explicit override.
- Contract-test the real service: a tool such as Schemathesis (the usual choice today; Dredd is an older alternative you may meet) sends requests generated from the spec to a test deployment and fails when responses do not match. This is the only check that catches code diverging from the spec.
- Generate an SDK in CI and compile it, so a spec that produces broken clients fails before release.
- One owner and one review path: spec changes go through the same pull request as the code that implements them.
Trade-offs and pitfalls
- Descriptions rot faster than types, so lint for presence and review for accuracy.
- A hand-edited published copy of the spec is the classic drift source: publish only from the repository build.
- Over-strict schemas make clients fail when the server adds a field:
additionalProperties: falsemeans "any field not listed here is invalid", so a validating client rejects a response the moment the server adds a harmless new field. Decide deliberately.
Here is an API error: HTTP 400 with the body {"error": "invalid request"}. Critique it from a developer's point of view, rewrite it so someone can fix the problem without contacting support, and explain how you would keep your error messages from leaking sensitive detail.
Sample Answer
Direct answer
The error {"error": "invalid request"} with a 400 tells the developer that they are wrong but not what, where or how to fix it. A good error names the field, says what was expected, gives a stable machine-readable code, carries a request ID for support, and links to help. The safe way to do this is to build errors from a controlled catalogue of messages you wrote, never from raw exception text (the internal error message your code throws, which can reveal how your system is built).
Critique
- Tautology. "Invalid request" restates the 400. No new information.
- No location. Which field or header? Maybe the JSON did not even parse.
- No cause or fix. What was wrong and what is valid?
- No machine-readable code, so client code cannot branch on the failure without string-matching the message.
- No request ID, so support cannot find the log line, which forces a ticket.
- No documentation link.
Rewrite
{
"error": {
"code": "invalid_parameter",
"message": "amount_cents must be an integer of at least 1. Received the string \"25.00\".",
"param": "amount_cents",
"request_id": "req_8f3a2c",
"doc_url": "https://docs.example.com/errors/invalid_parameter"
}
}
The response also carries Content-Type: application/json (or application/problem+json, the media type of RFC 9457 "Problem Details for HTTP APIs", if you follow that standard; a media type is the label in the Content-Type header that says what format the body is in, and an RFC is a numbered internet standards document). For several problems at once, return an array of {param, code, message} items so the developer fixes everything in one pass rather than one round trip per mistake.
Keeping errors from leaking sensitive detail
- Catalogue, not exceptions. Map each failure class to a code and a message a person wrote. A catalogue is just a lookup table kept in your code, for example:
invalid_parameter -> "{param} must be {rule}."
not_found -> "No such resource."
rate_limited -> "Too many requests. Retry after {n} seconds."
The code picks the row and fills in only safe values such as the field name. Anything not in the table becomes a generic internal_error plus the request ID. Never pass an exception's text to the client: it can contain SQL, file paths, hostnames or stack traces (the listing of internal code locations printed when a program crashes).
2. Log the detail server-side under the request_id. The developer gets the ID, support gets the internals.
3. Do not echo secrets. If the invalid value could be a token or password, name the field but do not repeat the value. Truncate long echoed input.
4. Avoid enumeration (letting an attacker learn what exists by trying guesses and reading the differences in your answers). Do not say "no account with that email" versus "wrong password" on login; return the same message. Return 404 for another tenant's resource rather than 403, so existence is not confirmed. A tenant is one customer account in a system shared by many. If customer A requests /invoices/1042 and it belongs to customer B, a 403 Forbidden tells A that invoice 1042 exists (just not for them), so A can probe IDs and map B's data. A 404 Not Found is the same answer A would get for an ID that never existed, so nothing leaks.
5. Keep validation messages about the caller's own input, never about internal structure ("column customers.tax_id is null").
6. Test it. Send malformed and hostile inputs in CI and assert responses never contain a stack trace, file path or internal hostname. CI (continuous integration) is the automated test run on every code change.
Pitfalls
- Messages helpful to attackers ("SQL syntax error near...").
- Changing messages breaks clients that parse them: that is why the code, not the message, is the stable part.
Design the support model for an external developer programme so that most questions are answered without a human. Where do docs, community and tooling fit, what would you record on the tickets that do reach people, and how do you turn those tickets into doc fixes?
Sample Answer
Direct answer
Build support in layers so each layer catches what the one before missed: self-service docs and error messages first, community second, a small human team third, engineers last. Record structured fields on every ticket that reaches a person, and run a weekly loop in which each ticket that exposes a docs gap becomes a doc fix with an owner. The goal is fewer tickets per developer over time, not fewer ways to contact you. Deflection means a question answered without a ticket (through docs, search, community or an assistant).
Layers
| Layer | What it holds | Notes |
|---|---|---|
| 0. Self-service | Docs, searchable error reference, sample apps, status page (a public page showing whether the API is up), changelog (a dated list of API changes) | Error messages link straight to their docs entry and carry a request ID |
| 1. Community | Forum or chat, staff seeding answers, marked accepted answers | Community answers feed back into docs |
| 2. Support team | Ticket queue with response targets by plan (for example, first reply within 1 business day for free plans, within 4 hours for paid) | Handles account, billing and integration problems |
| 3. Engineering | Escalation for real bugs | Only via support, with logs attached |
Docs-as-code means the docs are text files in a repository, reviewed and published like software, so anyone can propose a fix as a pull request (a reviewable change proposal). A request ID is a unique identifier returned with every response so support can find that exact call in the logs. Tooling: in-product links from errors to docs, a docs search that logs no-result queries, and optionally an assistant that answers from the docs only, cites the page and hands off to a human with the conversation attached. It needs monitoring for wrong answers; a confident wrong answer costs more than a ticket.
What to record on tickets that reach people
- Category from a fixed list (authentication, errors, webhooks, SDK, billing, other). An SDK is a client library that wraps the API in a language such as Python.
- Endpoint, error code and request ID.
- SDK name and version, language, sandbox or production (sandbox is the safe test environment with fake data).
- Journey stage: onboarding (first setup, before the first successful call) or live integration (already sending real traffic).
- Docs page read, or "none found".
- Resolution type: doc missing, doc exists but not found, doc wrong, real bug, feature request, user error.
- Time to resolve, and whether a link alone answered it.
Turning tickets into doc fixes
- Weekly, review tickets tagged doc-related.
- Group by topic. If a topic reaches three tickets in a month, open a docs task with an owner.
- Let support staff propose the fix as a pull request, since docs live as code.
- After the fix, watch that topic's ticket count fall.
Filled-in ticket (illustrative): category webhooks; endpoint POST /v1/messages; error invalid_signature; request ID req_8f3a; SDK python 2.4.1; environment production; journey stage live integration; docs page read none found; resolution type doc exists but not found; time to resolve 12 minutes; a link alone answered it yes. Ten of these with the same resolution type is the signal to move or rewrite the page.
Worked example (illustrative): 1,000 active developers, 80 tickets in a month, which is 8 per 100. Tags: 30 docs-related, 20 bugs, 15 user error, 15 feature requests. Within the 30, webhook signature verification accounts for 12. Rewrite that page with a working code sample and put the link in the signature error. If next month's webhook tickets fall from 12 to 5, that removes 7 tickets: 73 tickets, 7.3 per 100.
Measuring deflection honestly
True deflection (someone solved it without a ticket) is hard to see. Use tickets per 100 active developers, docs-page exits without a contact, and the satisfaction score on self-service answers (the share of readers who click "this answered my question" on a docs page or assistant reply). Do not hide the contact form to make numbers look good; developers then leave silently.
Pitfalls
Tags that nobody fills in (make the fields required and short), a doc backlog with no owner, and counting community answers as resolved when the answer was wrong.
How would you define developer experience for an API product, and which handful of metrics would you track to know whether it is improving?
Sample Answer
Direct answer
Developer experience (DX) for an API product is the quality of the whole journey a developer has with it: discovering it, getting a first call working, building an integration, operating it in production, troubleshooting, and upgrading. It is not just docs or pretty SDKs. Judge it by outcomes across that journey, using about five metrics, one per stage, each with a clear numerator (the top of the fraction) and denominator (the bottom). An integration below means one customer's application connected to your API, and a developer usually has one or more. Two status-code families appear: 2xx means the request succeeded and 4xx means the caller made a mistake (bad input, missing auth).
The metrics I would track
| Stage | Metric | Definition |
|---|---|---|
| Get started | Time to first successful call | Median and 90th percentile (p90, the time by which 90% finish) minutes from first docs view to first 2xx |
| Activate | 7-day activation rate | Signups making a successful call within 7 days, divided by all signups |
| Build | Support contacts per 100 active integrations | Tickets about how to use the API in a month, divided by integrations that made a call that month, times 100 |
| Operate | Client-error share for new integrations | 4xx responses divided by all responses, for integrations in their first 30 days |
| Evolve | Deprecation migration rate | Integrations moved off a deprecated version by its sunset date, divided by those affected |
Add a small survey (one question, "how easy was it to integrate?") for the human view the numbers miss, but keep it a companion, not the target.
My north star (the one number the team steers by): the activation rate, with the time to first successful call as its speed companion. Activation says whether developers succeed at all; time says how painful it was.
Worked example of the arithmetic
A month with 200 signups, 120 of whom make a successful call within 7 days: activation is 120 / 200 = 60%. The docs change; next month 220 signups and 154 activate: 154 / 220 = 70%. Support: 40 how-to tickets across 500 active integrations is 40 / 500 x 100 = 8 per 100. Numbers are only comparable when denominators are defined the same way each month.
The other three, with made-up numbers:
- Time to first successful call. Ten new developers take 4, 5, 6, 7, 8, 9, 11, 14, 22 and 45 minutes. The median is the middle of the sorted list, (8 + 9) / 2 = 8.5 minutes. The p90 (nearest-rank method) is the 9th of 10 values, 22 minutes, so the slowest tail is what a docs fix should target.
- Client-error share. New integrations sent 1,000 responses in their first 30 days and 150 were 4xx: 150 / 1,000 = 15%. If clearer error messages and examples cut that to 90 of 1,000, it is 9%. A high 4xx share means developers keep sending requests the API rejects, which points at unclear docs or errors (it does not count server failures, which are 5xx and a reliability issue).
- Deprecation migration rate. A deprecated version is an old API version you have announced will be retired, and its sunset date is the day it stops working. If 40 integrations were on v1 and 30 had moved to v2 by the sunset date, the rate is 30 / 40 = 75%. The 10 left behind are the ones to contact before you switch it off.
How each is gamed, and the guard
- Time to first call can be improved by counting a trivial ping: require a real endpoint.
- Support contacts fall if you make contacting support hard: pair with the survey and with client-error share.
- Activation rises if signups are filtered harder: watch signup volume alongside it.
Pitfalls
- Vanity metrics (numbers that look impressive but do not show developers succeeding, such as page views, downloads, GitHub stars) that say nothing about success.
- Twenty metrics: nobody acts on them. Five, one per stage, each with an owner.
Unlock Full Question Bank
Get access to all 36 API Documentation and Developer Experience interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.