API Design and Versioning Guide
Define API representations, pagination, credentials, and version selection, then migrate consumers through compatibility tests, deprecation, and a controlled retirement.
trigger="A new API needs a stable contract, or an existing interface must change without an uncontrolled consumer break." owner="The API owner responsible for supported behavior and the migration decision." participants={["Provider engineer", "Consumer maintainers", "Security reviewer", "Developer-experience owner", "Release operator"]} prerequisites={[ "Consumer use cases, data ownership, and security requirements are documented.", "The existing contract, client versions, and relevant production behavior are available for comparison.", "A test environment can run old and new consumers against candidate provider versions." ]} outputs={[ "A versioned interface definition with representations, errors, pagination, and credential behavior.", "A compatibility matrix and tested old-to-new migration example.", "A deprecation and retirement record with communication, stop conditions, and recovery." ]} doneWhen={[ "Supported clients pass contract and behavioral tests for the proposed deployment.", "Version selection and cache behavior are explicit through clients and intermediaries.", "Consumers have a verified migration path and an accountable support contact.", "Retirement is approved from usage, contractual, and recovery evidence rather than a date alone." ]} />
Scope: a contract that can evolve
Use this playbook to make an API's behavior explicit and to change it deliberately. A version number is only a selector. It does not establish compatibility, isolate stored data, or tell a consumer how to recover from an uncertain write.
The output is a tested interface and migration plan. It is not a claim about client engagement averages or a promised reduction in support tickets. Measure onboarding effort, failed integrations, and support demand against the API's actual baseline if those outcomes matter.
For ownership and release coordination across teams, use API Design for Large Teams. This guide focuses on concrete design and migration decisions.
1. Define resource and operation semantics
The provider engineer begins with a user operation, not a URL naming exercise. Identify the resource, its authoritative owner, allowed transitions, and consistency requirements. Document which actions are read-only and which create durable or external effects.
Use a consistent naming convention, but do not force every business action into an artificial CRUD shape. A request to cancel an order needs cancellation semantics: when it is allowed, whether completion is asynchronous, and what happens if fulfillment already started.
| Contract area | Specify before implementation | | --- | --- | | Identifiers | Scope, stability, opacity, and whether clients may persist them | | Fields | Type, units, nullability, omission, defaults, and access rules | | Collections | Filter, stable ordering, pagination, and consistency under change | | Writes | Validation, concurrency control, idempotency, and unknown outcomes | | Errors | Stable meaning, safe detail, correlation, and retry eligibility | | Limits | Payload, concurrency, batch, deadline, and quota behavior | | Lifecycle | Version selection, support state, and migration policy |
Keep examples small enough to execute, but include at least one invalid request and one permission-denied case. Document behavior outside the schema, including whether a successful response means work completed or was merely accepted.
2. Keep representations and errors predictable
Choose a response shape that suits the API and keep it stable. An envelope can separate data from pagination or other metadata, but it is not mandatory. Avoid including changing timestamps or request identifiers in a representation unless consumers need them and caching implications are understood.
Separate writable properties from returned properties. Explicitly validate allowed fields and enforce field-level authorization. Do not bind an arbitrary request body directly to a persistence model.
For HTTP APIs, RFC 9457 Problem Details provides a standard error representation. A minimal illustrative response might be:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Cache-Control: no-store
{
"type": "/problems/invalid-request",
"title": "The request contains invalid fields",
"status": 400,
"detail": "Check the documented format for scheduledAt.",
"requestId": "req_example"
}The problem type and extension field above are application-defined examples, not prescribed values. Return safe detail rather than echoing secrets or personal data. Document which errors can be retried and whether an operation identifier is required. A transport timeout after a write may mean the outcome is unknown, not that nothing happened.
3. Design pagination for the collection's behavior
Offset pagination is easy to explain and can suit a small, stable collection. With concurrent inserts or deletions, positional pages can shift. Cursor or keyset pagination can avoid some positional problems, but a cursor alone does not guarantee a consistent snapshot of a changing collection.
Define a deterministic ordering with a tie-breaker, such as creation time plus a stable identifier. State whether updates can move records between pages and whether a snapshot or cutoff is provided. Exports requiring a complete fixed dataset may need a separate snapshot-based job.
| Decision | Required behavior | | --- | --- | | Page size | Document the default, maximum, and handling of invalid values | | Continuation | Return a next-page token only when continuation is available under the contract | | Token context | Bind or validate the relevant filter, sort, tenant, and version context | | Expiry | Explain how the client restarts when a token is no longer valid | | Authorization | Recheck access on each page; possession of a token is not permission | | Mutation | Specify possible duplicates, omissions, or snapshot guarantees |
Treat page tokens as opaque to clients. Protect them from tampering when they encode trusted state, and do not put sensitive data into a token merely because it is base64-encoded. Google's pagination guidance explains the value of opacity and documents continuation behavior.
Test equal sort values, deleted records, changed filters, revoked access, and expired tokens. A pagination test using an unchanging fixture is useful but does not cover those cases.
4. Choose version selection and default behavior
Select a scheme that clients, documentation, gateways, and support tooling can observe. A dated contract is a versioning policy that may be carried in a header or another explicit selector; it is not a separate transport.
| Approach | Useful property | Tradeoff to handle | | --- | --- | --- | | URI major version, such as /v1/orders | Visible in routes, examples, and access logs | Parallel routes and common resource identity need a deliberate design | | Explicit request header | Stable resource URL while selecting behavior | Clients and intermediaries must forward, log, and cache the selector correctly | | Dated contract selection | Names a defined behavior snapshot and migration target | Requires clear date semantics, supported snapshots, and tooling to identify the selected contract |
Document the exact behavior for missing, malformed, unsupported, and retired selectors. Do not silently route an unknown version to “latest.” If a default exists, pin and disclose it so a provider release does not unexpectedly change an unversioned client's behavior.
For header-based representations, configure caches to distinguish the relevant selector and obey the selected cache policy. Test through the actual intermediary, not only against the origin. RFC 9111 describes cache keys, Vary, and reuse conditions. A correct version selector does not by itself make personalized content suitable for shared caching.
5. Prove compatibility with a concrete transition
A field rename is a behavior change. For example, an existing response may be:
{ "id": "customer_example", "name": "Example account" }If the team wants displayName, a transition can initially preserve name with its existing meaning while adding the new field:
{
"id": "customer_example",
"name": "Example account",
"displayName": "Example account"
}This is a candidate migration, not automatic proof of compatibility. Verify strict clients accept the extra property and that consumers agree on meaning. If the new field has different semantics, document that distinction rather than keeping two names deceptively synchronized.
Google's backwards-compatibility guidance identifies removal, renaming, defaults, and enum evolution as relevant constraints. Test schema and behavioral changes against the supported client set. An “additive” diff can still break a closed enum, a signature calculation, or client-side validation.
Maintain a matrix of old/new provider and consumer versions. Verify the pair used during rollout, the intended final pair, and the pair used during rollback. Do not remove the old field or behavior until its supported consumers have migrated or the approved retirement policy permits removal.
6. Define credentials by authority, not their label
A key described as “publishable” is safe to expose only because the API deliberately gives it limited public capabilities. It is not a general rule for API keys. A public identifier cannot prove the caller controls a protected account; a bearer secret grants authority to whoever possesses it.
Keep privileged secrets out of browser code, URLs, examples, and telemetry. Document issuance, scope, rotation, revocation, and recovery. Enforce tenant, object, and action authorization on the server regardless of whether the request contains a valid credential.
For delegated access, use a reviewed OAuth design with the client's security properties in mind. RFC 9700, OAuth 2.0 Security Best Current Practice covers modern protections, including PKCE and redirect-related risks. A public browser client cannot keep a shared client secret confidential. Use the appropriate flow and server-side authorization; do not substitute a “public key” label for that design.
Test revoked, expired, wrong-audience, wrong-tenant, and insufficient-scope credentials. Credential-lifetime and rotation decisions come from the threat model and platform contract, not a fixed duration copied across every API.
7. Publish a migration that consumers can execute
The developer-experience owner prepares a change log, old/new examples, affected operations, SDK guidance, and a troubleshooting path. Include differences in errors, pagination, limits, side effects, and defaults. Verify examples against the candidate implementation in an isolated environment.
Give consumers a way to identify their selected contract and deployment version without exposing credentials. Use telemetry and maintainer confirmation together to track adoption. Do not use live shadow writes to compare versions unless duplicate effects are explicitly prevented.
The Deprecation header standard communicates that a resource is or will be deprecated without itself changing its behavior. The Sunset header standard communicates when a resource is expected to become unresponsive. They use different date formats.
The following is an illustrative notice, not an Ampity API commitment:
Deprecation: @1803859200
Sunset: Thu, 01 Jul 2027 00:00:00 GMT
Link: <https://example.com/api/migrations/orders>; rel="deprecation"The deprecation timestamp represents March 1, 2027 at 00:00 UTC. Choose real notice periods from the support policy and consumer obligations. Headers supplement communication; not every client exposes them to its maintainer.
8. Stop, recover, and retire safely
| Failure | Stop condition | Recovery | | --- | --- | --- | | Old client rejects a changed response | Supported compatibility test or production signal fails | Restore the compatible representation | | Version header is dropped | Requests select the wrong behavior | Correct intermediaries and invalidate affected cached variants | | New write semantics produce incorrect state | Business invariant fails | Halt affected writes and reconcile records | | Migration stalls for a critical consumer | Retirement would break a supported workflow | Resolve the exception before approving shutdown | | Old implementation cannot read new data | Rollback compatibility is absent | Use the documented forward-recovery path |
A routing rollback does not undo committed business changes. Before deploying new write behavior, define reconciliation and preserve a record of affected operations. Before retiring a route, verify scheduled and infrequent consumers, support obligations, and the response policy after retirement.
Archive the supported contract, migration record, and relevant evidence under the organization's retention rules. Remove old implementation only after the rollback or support need has ended. Do not assume a fixed period of no traffic proves that deletion is safe.
9. Turn the compatibility claim into a release fixture
For the name to displayName example, the provider engineer creates one fixture per meaningful state: a normal value, an empty value if allowed, a missing optional value, non-ASCII text, and a value visible only to a privileged role. The consumer maintainer runs the actual supported parser and business operation, not a substitute client written just for this test. The evidence must identify both builds and the fixture revision.
Use this matrix as the release artifact. “Pass” means the documented operation produces the expected meaning and effect, not merely an HTTP success response.
| Consumer and provider pair | Assertion | Decision if it fails | | --- | --- | --- | | Existing consumer with existing provider | Baseline behavior is reproducible | Repair the fixture or understand existing behavior before comparing | | Existing consumer with candidate provider | Old fields, defaults, and errors retain their contract | Stop rollout or introduce a separately selected contract | | Migrated consumer with candidate provider | New behavior is used and checked deliberately | Keep that consumer on the old path | | Migrated consumer with recovery provider | The declared recovery pair still works | Add a consumer fallback or choose forward recovery before release |
For writable fields, resolve ambiguous input explicitly. If a transition accepts both name and displayName, what happens when their values disagree? Rejecting contradictory input, accepting only the version's documented writable field, or applying a documented precedence rule are different contracts. Choose one and test it. Do not let object-property order or a framework binding decide which value reaches storage.
Keep persistence migration separate from representation migration. Adding a response alias does not justify dropping a column or rewriting stored meanings. The database owner identifies the last reversible point and verifies that the recovery build can read records created during coexistence. If it cannot, the release record must name the forward-repair path and the operator authorized to stop writes.
10. Exercise pagination, caching, and uncertain writes together
The API test owner prepares a small collection with repeated creation times and a deterministic identifier tie-breaker. Read the first page, then insert a record, delete a record, and change a sortable field before requesting the next page. Compare the observed result with the promised collection semantics. If the interface offers a live view, document the allowed changes. If the product promises a complete export, test the snapshot or export-job boundary instead of silently strengthening the meaning of a cursor.
Repeat continuation with another tenant, a changed filter, and a revoked credential. The expected result must follow the documented token-context and access policy. Preserve sanitized request and response pairs plus the database fixture. A client must be able to distinguish an expired continuation from an empty collection so it can restart deliberately rather than report that no records exist.
For versioned responses, send the same resource request through the real gateway and cache using each supported selector. Inspect the response shape, effective cache policy, and cache-hit behavior. Then test a missing selector, an unknown selector, and two identities with different access. Stop the migration if the intermediary mixes representations or exposes another identity's data. Disabling affected caching can be a containment option, but first verify the origin can handle the resulting load.
Finally, exercise an operation with an uncertain write outcome. Use the API's documented operation identity, interrupt the response after the server commits in the authorized test environment, and retry under the same identity. Assert the business effect and the returned operation status. Reuse the identity with changed parameters and check the documented rejection behavior. The test must also cover identity expiry; a client retry outside the supported window needs a reconciliation decision rather than an assumption of duplicate protection.
The release operator packages these results by contract version. A summary saying “all endpoint tests passed” is insufficient if it hides which caches, client builds, and failure boundaries were tested.
11. Close the consumer retirement record
For each consumer, record the maintainer, supported contract, observed operations, scheduled-job frequency, last successful migration rehearsal, and unresolved exception. Include disaster-recovery jobs and rarely used administrative tools. An absence of recent traffic cannot tell you whether a quarterly process was forgotten or intentionally retired.
Before shutdown, the API owner reviews both observed adoption and explicit maintainer acknowledgments. Identify contractual support duties and unknown consumers separately. Where external consumers cannot all be identified, document the notice channels, usage evidence, residual risk, and approved post-retirement response. Do not invent a maintainer acknowledgment to close the worksheet.
The next action is a bounded consumer rehearsal using the replacement contract and the declared recovery pair. Retire only after the owner can explain what will happen to a late caller, who will respond, and which recovery option remains available. Keep the record with the released contract so future maintainers can distinguish an intentional retirement from an accidental routing failure.
Migration record and completion checklist
API owner / old contract / replacement contract:
Business reason and exact behavior changes:
Version selector and missing/invalid-selector behavior:
Affected consumers, client libraries, and maintainers:
Compatibility matrix and test evidence:
Pagination, credentials, caching, and retry changes:
Migration examples and communication record:
Stop conditions and safe rollback pair:
Data reconciliation or forward recovery:
Deprecation, sunset, exceptions, and retirement approval:"Representations, units, defaults, errors, and side effects are documented.", "Pagination covers mutation, token expiry, and authorization boundaries.", "Version selection is explicit and verified through intermediaries.", "Supported clients pass additive-change and behavior tests.", "Public identifiers and privileged credentials have distinct authority rules.", "Migration examples run, consumer adoption is evidenced, and exceptions are owned.", "Retirement and rollback account for shared state and committed effects." ]} />
Ampity's backend systems and APIs service is the related service for a workload-specific contract and migration. Standards and examples support the design; consumer verification and an owned release decision remain necessary.