Skip to content

Define service-to-service API standards - #868

Open
mike-gorman-bitwarden wants to merge 4 commits into
mainfrom
docs/api-standards
Open

mike-gorman-bitwarden wants to merge 4 commits into
mainfrom
docs/api-standards

Conversation

@mike-gorman-bitwarden

@mike-gorman-bitwarden mike-gorman-bitwarden commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

📔 Objective

To define API standards for internal APIs (not exposed to the internet).

Note to the reviewer

When faced with a 1,400-line standard, it is easy to feel overwhelmed, or that it imposes too much of a burden on the developer. I would agree if compliance to the standard required writing all of the code yourself. But it is our intention to equip you with a framework and prescriptive programming model to give you as much compliance as we can "for free" with the guarantee that no request ever reaches your code that does not conform to the declared request model. Compliant error handling should be just throwing an exception and, for everything else, classes and helper methods to make it easy.

So, as you're reviewing the standard, try not to think about what code will be needed to conform but whether the standard is a good standard? Does it make sense? Is it necessary? Is it overly restrictive? Should it be MUST rather than SHOULD, or SHOULD rather than MUST? Or some completely different standard? Our commitment to you is a forthcoming framework and programming model that will make it easy to write compliant APIs.

@mike-gorman-bitwarden
mike-gorman-bitwarden requested a review from a team as a code owner September 25, 2026 13:12
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Deploying contributing-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: ecdbc9a
Status: ✅  Deploy successful!
Preview URL: https://4ebd3cc2.contributing-docs.pages.dev
Branch Preview URL: https://docs-api-standards.contributing-docs.pages.dev

View logs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR adds docs/architecture/server/service-to-service-api-standards.md, the living standard adopted by ADR-0036, covering paths, versioning, JSON shape, responses, filtering, paging, advanced queries, bulk operations, errors, and deprecation. Review focused on internal consistency of the normative rules, correctness of the cited RFCs and JSON:API divergences, and the Docusaurus mechanics (frontmatter, sidebar position, cross-references). Every internal anchor and the ADR-0036 link resolve, sidebar_position: 3 does not collide with the existing server docs, and Build, Lint, and Aikido are green. One contradiction between two MUST rules is worth resolving before merge; the other two findings are smaller consistency items.

Code Review Details
  • ⚠️ : "Field not present" ≡ null contradicts the partial-update semantics PATCH and bulk update rely on — composed, the rules say an omitted field clears itself
    • docs/architecture/server/service-to-service-api-standards.md:178-179
  • ♻️ : Filter examples use birthDate, violating the document's own rule that date fields end in At
    • docs/architecture/server/service-to-service-api-standards.md:615-616
  • ❓ : Unclear whether a bulk update may name version; it is explicitly not readOnly, so it passes the read-only rejection rule but is then silently ignored
    • docs/architecture/server/service-to-service-api-standards.md:1054-1062

Comment thread docs/architecture/server/service-to-service-api-standards.md Outdated
Comment thread docs/architecture/server/service-to-service-api-standards.md
Comment thread docs/architecture/server/service-to-service-api-standards.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Service-to-service API standards

{/* cspell:ignore reate reates eletes elete pdate pdates fieldsets */}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Same comment from earlier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. 480cb7b

@audreyality audreyality left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 I took a look at the broad content in this review, without exploring the REST semantics.

Comment on lines +95 to +97
> **Why versioning in the path?** It is visible in logs, traces, routing rules and curl commands; it
> needs no content negotiation to read; and it lets two versions coexist behind one host. Header and
> media-type versioning are both defensible but harder to operate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Not specific to this standard: we should also log API version independent of the URL so that we can query it without having to parse the URL.

Comment on lines +9 to +11
**Scope.** Service-to-service APIs, whose callers are other Bitwarden services. Bitwarden's existing
public API is out of scope and is not changing, as is the internet-reachable surface our own clients
call — the part the server repository refers to as "internal".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ We should also exclude APIs whose express intent is to implement a standard from this scope. These will likely be related to infrastructure, such as OpenTelemetry or health checks.

Comment on lines +48 to +49
return - for both the happy path and the not-so-happy path. Developers `SHOULD` strive to think in
terms of resources and be on guard against **API proliferation** that can result from over-tailoring

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ What does it mean to "think in terms of resources"? This is one of those thorny questions that REST kind of hand-waves, and so many folks have their own hand-wavy version of what constitutes a resource.

Especially difficult to define are situations where resources have integrity requirements with an impedance mismatch between REST and their persistent representation.

Think of the difference between a JSON-LD link and a database foreign key. JSON-LD links are RESTful and much more expressive as a result. So if you think of references in terms of hypertext, you get a very different set of constraints than someone who thinks of them as database entities.


### Operation identifiers

Every operation `MUST` carry an explicit, stable `operationId`. "v1" APIs `SHOULD NOT` include the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Is the operation identifier the same as a version identifier? If not, what makes each identifier distinct? Is the operation identifier composed of path information?

❓ I think there's a term mismatch here?

Suggested change
Every operation `MUST` carry an explicit, stable `operationId`. "v1" APIs `SHOULD NOT` include the
Every operation `MUST` carry an explicit, stable `operationId`. "v1" APIs `SHOULD` include the

If I'm making a new interface, then it should include the version. If it shouldn't, that's an exception that requires justification with the architecture group.

/api/v1/users/123/addresses
```

Paths `SHOULD` be "hackable". If `GET /api/v1/users/123/addresses/456` returns the details about

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Minor language tweak

Suggested change
Paths `SHOULD` be "hackable". If `GET /api/v1/users/123/addresses/456` returns the details about
Paths `SHOULD` be traversable. If `GET /api/v1/users/123/addresses/456` returns the details about

🤔 We need to take care in these standards that they cannot be used to enumerate sensitive data structures. Take, for example, a construction as follows:

  • GET /api/v1/vault/123/ciphers/112
  • GET /api/v1/vault/123/ciphers/235
  • GET /api/v1/vault/123/ciphers/5813

I should not get permissions failures when I examine these ciphers without access to vault/123, because that would let me know that 112 is a valid cipher for the vault, but 113 is not. Instead, the entire ciphers sub-resource should not exist (404). While this property is useful, it must be designed with care.

🎨 This is a situation where it makes sense to explore the cross-cutting concern. The pieces are explained, but disconnected, and so the complexity of URL design comes from inference. Since, for many of our APIs, zero-knowledge is a governing non-functional requirement, it's important that this complexity is directly described by the standard.

Comment on lines +138 to +139
Otherwise, if changes need to be made that _would_ be breaking changes, a new version of the API
`MUST` be created and the old one [deprecated](#deprecation).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ This buries the lede after a large number of exceptions qualifying SHOULD NOT

Comment on lines +141 to +146
> **Adding a value to a constrained field deserves a second look.** It is additive, so it is not a
> breaking change by the definition above, and a caller that treats the field as an open string is
> unaffected. But a generated client that deserializes the field into a closed enumeration will fail
> on a value it has never seen — and it will fail at the client, on a change that looked safe from
> the service. Consider whether the callers of that field are tolerant of unknown values before
> adding one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 The client interfaces could be designed to allow for this circumstance, and a mechanism to do this baked-in. In particular, the client library should update with the new value 3 releases before the service can emit it. (This number is derived from our self-host compatibility policy. It could be shorter since this is an internal API.)

And this could be augmented with Roslyn lints prohibiting the new value on the server and warning about the new value in the client in the iterim.

Comment on lines +171 to +174
- Field values that are constrained to a fixed set of values `SHOULD` enumerate the valid values in
all caps (e.g. `RED`, `GREEN`, `BLUE`) both in the OpenAPI spec and in example JSON to help
distinguish these fields from free-text string fields. At runtime, however, APIs `MUST` ignore
case when validating these values.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Unless we also restrict symbols to ASCII, this isn't workable in practice. Unicode has all kinds of interesting casing rules.

🤔 I wonder if it would be better to use a structure to identify these fields instead of transforming their text value. Discriminated unions are a powerful way to ensure data shapes are constrained, and allowing any enum to become a discriminated union has proven very useful to me in API design.

// `kind` signals the constraint
{ "someFixedValue": { "kind": "Value" } }

// this works especially well when the new value has new required fields
{ "someFixedValue": { "kind": "NewValue", "required": "😎" } }

Comment on lines +184 to +189
- Fields holding a date or date/time `SHOULD` end in `At` (e.g. `createdAt`, `expiresAt`).
- Boolean fields `MUST NOT` be prefixed with `is` (e.g. `active`, not `isActive`).
- Fields whose value is the identifier of another resource `SHOULD NOT` be suffixed with `Id`. A
string-valued `assignedTo` is self-evidently the identifier of the user it is assigned to;
`assignedToId` adds nothing. This standard does not apply to fields that reference external
identifiers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 These conditions seem either arbitrary or contradictory. At is allowed, even though an ISO-formatted date is more self-evident than an identifier.

Comment on lines +195 to +197
- Because our APIs are largely based on JSON:API, services `MUST` accept a request whose
`Content-Type` is `application/vnd.api+json`, and `MUST` honor an `Accept` of
`application/vnd.api+json`, even though the API itself only advertises `application/json`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Why don't we advertise application/vnd.api+json if we accept it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants