# OpenAPI first for shared APIs across several teams

> OpenAPI first helps teams agree on payloads, auth, and error formats early, so frontend, backend, and partners stop shipping mismatched changes.

## Where shared APIs start to drift

Teams rarely break a shared API in one dramatic move. Drift starts with small decisions that feel harmless at the time. One squad adds a field for a checkout screen. Another renames it for a report. A third copies an older payload into a new endpoint because it ships faster.

Field names are usually the first crack. The same idea gets labeled three different ways, and every team thinks its version is obvious. A mobile app sends `userId`, the backend returns `customer_id`, and an admin tool expects `clientId`. None of those names is unreasonable on its own. Together, they create slow bugs, extra mapping code, and pointless arguments about which side is "wrong."

Missing values cause the next round of trouble. One client sends an empty string. Another sends `null`. A third leaves the field out entirely. If nobody wrote down what each choice means, every service makes its own guess. That guess becomes behavior. Maybe an empty value clears a profile field, while an omitted value leaves it unchanged. Users see odd results, and support tickets pile up because each client behaves a little differently.

Auth rules drift even faster because teams often explain them in chat instead of one shared spec. Someone says, "This endpoint needs a service token," but that note never reaches the docs. Another team builds against an old message, adds the wrong header, and gets blocked in test or, worse, in production.

Error responses are where drift turns expensive. One endpoint returns a plain message. Another wraps errors in an object. A third uses different names for the same problem, like `error`, `message`, or `detail`. Client code grows a pile of special cases just to show a basic error state.

That is how shared APIs get messy: small naming differences, unclear empty values, auth rules buried in chat, and error bodies that change from route to route. By the time QA finds the break, each team has already built around a different contract.

## What the spec should settle early

Teams move faster when the API contract settles arguments before they become bugs. An OpenAPI-first approach works best when the spec answers the boring questions up front, before backend, web, and mobile teams all guess differently.

Start with request and response shapes. Every field needs a type. The spec should say which fields are required, which can be null, and which get a default when the client sends nothing. If a field only accepts a small set of values, define the enum early. Dates need one format too. If one team sends `2026-04-10` and another expects a full timestamp, you get silent failures that waste days.

A useful review should settle a few things before implementation starts:

- the exact payload schema for each endpoint
- required fields, defaults, enums, and date formats
- one auth method and where credentials go
- one error response format with clear status code rules
- who can approve breaking changes

Auth rules need plain language. Pick one method for the API and write it clearly in the spec. If clients send a bearer token in the `Authorization` header, say that and keep it consistent. Do not let one service accept cookies, another accept query params, and a third expect a custom header unless there is a real reason.

Errors should be dull and predictable. A client should get the same body shape for a validation error, a permission error, or a missing record. Keep it simple: a stable error code, a human message, and optional field details. Then define status code rules people actually follow. For example, `400` for bad input, `401` for missing or bad auth, `403` for blocked access, `404` for missing resources, and `409` for conflicts.

Breaking changes also need an owner. One person, or a small review group, should approve any change that can break existing clients. If nobody owns that call, the API changes by accident.

## Agree on payloads before UI work starts

Most client bugs start with small mismatches, not big failures. One team sends `userId`, another expects `customer_id`, and the UI team guesses which one is right. A few days later, people add quick fixes on both sides, and the API gets harder to trust.

Pick one field name for one idea and keep it everywhere. If a person is `userId` in sign-up, it should not become `ownerId` or `accountUserId` in the next endpoint unless it really means something different. It sounds picky, but it saves a lot of wasted time across web, mobile, and backend work.

Required fields need the same discipline. Mark a field as required only if every client can send it every time. If the mobile app creates a draft before the user finishes a form, that field is not really required yet. Calling it required in the spec only pushes teams into fake values, empty strings, or brittle workarounds.

Examples matter more than many teams expect. Do not stop at type definitions. Add request and response examples that look like real screens people will build. If the checkout form has an optional company name, show that. If the profile screen lets users save without a phone number, show that too.

A few rules need explicit answers in the spec. Say when clients should send `null` and when they should omit a field. Decide whether an empty string is allowed or whether clients should send no value at all. Define what an empty list means. Does it mean "none," or does it mean "not loaded yet"? And pick one date format for everyone.

Renaming fields is where shared APIs often fall apart. Before you remove or rename anything, add a deprecation note in the spec and give clients time to move. A calm transition beats a "small cleanup" that breaks three teams on release day.

## Write auth rules people can follow

Auth rules should remove guesswork. The spec needs to show which endpoints are public, which need login, and which are limited to a smaller group of users. If one team treats `/profile` as protected and another assumes it is public because it is a `GET` route, the client will break in small, annoying ways.

Write the rule beside each endpoint, not in a separate document people forget to check. Public routes should say they need no auth. Protected routes should say exactly what they accept.

A simple pattern works well:

- Public endpoints: no token or cookie required
- Signed-in user endpoints: Bearer token in `Authorization: Bearer <token>`
- Browser session endpoints: cookie named `session_id`
- Admin endpoints: Bearer token with `admin` scope

Be exact about the token type and where it goes. "Use auth" is vague. "Send a Bearer token in the Authorization header" is clear. The same goes for cookies. If the server expects `session_id`, write `session_id`.

Error rules matter just as much as login rules. Clients need to know when to prompt for login, when to hide an action, and when to retry later. A good baseline is simple: use `401` when the request has no valid credentials, `403` when the user is authenticated but lacks the required role or scope, and `429` when the client hits a rate limit.

Roles and scopes should read like plain English. `projects:read` can view project data. `projects:write` can create and edit projects. That is much easier to work with than short internal labels nobody understands.

Keep the same auth model across similar endpoints. If `/projects/{id}` uses a Bearer token, `/projects/{id}/tasks` should not suddenly require a cookie unless there is a real reason. When auth rules stay consistent, people spend less time decoding failures and more time building the product.

## Make errors boring and consistent

When several teams share one API, messy errors create more support work than the main feature. One endpoint returns a plain string, another sends a nested object, and a third hides the useful part in a debug message. Clients start adding one-off fixes, and those fixes stick around for months.

Use one error body shape across every endpoint, even when the status code changes. With an OpenAPI-first process, you can define that shape once and reuse it everywhere, so web, mobile, and backend teams all parse errors the same way.

A simple pattern is often enough:

```json
{
  "error": {
    "code": "EMAIL_ALREADY_USED",
    "message": "This email is already registered.",
    "fields": {
      "email": "Already in use"
    },
    "request_id": "req_7F3K92"
  }
}
```

The human message can change over time. The machine-readable code should not. If a client checks for `EMAIL_ALREADY_USED`, keep that code stable. Changing text is usually safe. Changing codes breaks logic in apps, tests, and admin tools.

Status codes need strict meaning too. If `401` means missing or bad credentials, keep it there. If a user is signed in but blocked from an action, return `403`. If input fails validation, pick `422` or `400` and stick to that rule. Teams move faster when they do not have to guess what each endpoint meant.

Field errors matter most for forms. A general message like "Validation failed" helps nobody when the screen has six inputs. Return field-level details so the client can point to the exact problem and show a useful message beside the right input.

Request IDs are worth adding when support or ops teams need to trace a failure. A user can paste the ID into a ticket, and the team can find the matching logs without a long back-and-forth.

Boring errors are easier to ship, easier to test, and much cheaper to support.

## Run a simple OpenAPI-first workflow

Teams move faster when they agree on the API before anyone writes feature code. A short review at the start can save days of rework later, especially when backend, web, mobile, QA, and partner teams all depend on the same endpoint.

Treat the spec as part of the feature, not paperwork after the fact. If a field name, auth rule, or error shape is still under debate, settle it in the spec before people build around guesses.

A simple workflow looks like this:

1. Draft or update the OpenAPI spec as soon as the feature is scoped.
2. Review it with every team that will touch it, including QA and any external consumer.
3. Freeze names, required fields, auth rules, and error formats for that release.
4. Share mocks or sample responses so frontend and mobile work can start early.
5. Check the real implementation against the spec before release.

This does not need a long meeting. In many teams, 20 to 30 minutes is enough if the spec is clear. One person walks through the request body, one person checks auth, and QA looks for missing error cases. That small step catches most of the ugly surprises.

A realistic failure looks minor at first: the backend adds `account_status`, the web app expects `status`, and the mobile app treats missing auth as `403` while the server returns `401`. None of that sounds dramatic until three clients ship different fixes. If the spec locks those details first, the whole change stays boring, which is exactly what you want from an API.

## A realistic example with three teams

A common mess starts the same way. A web team, a mobile team, and a partner portal team all need the same orders API. They move at different speeds, they ask for slightly different fields, and each team fills gaps in its own way.

The first disagreement often sounds small. The web team asks for `amount` because it reads well in the UI. The mobile team wants `total_cents` because it avoids rounding bugs. The partner team already built a report around `total`. If nobody settles it early, the API grows three meanings for one number.

An OpenAPI-first approach stops that drift before code spreads it everywhere. The spec picks one field name, one type, and one currency rule. For example, the teams agree that every order response returns `total_cents` as an integer and `currency` as a 3-letter code like `USD`. Nobody sends floating-point money values, and nobody guesses whether `amount` means dollars, cents, or a formatted string.

Auth usually follows the same pattern. One client sends `Authorization: Bearer <token>`, another tries an API key header copied from an older service, and the partner portal adds a session cookie because it was faster. That can appear to work for a week. Then support tickets start. The spec should lock this down to one rule for every client that uses the API.

Error handling gets messy even faster. If the web app gets `{ "message": "Order not found" }`, the mobile app gets `{ "error": "missing" }`, and the partner portal gets an HTML 403 page from a proxy, each team builds different fallback logic. A shared error body fixes that. Every client can expect the same shape, such as `code`, `message`, and `request_id`.

QA can then test the same OpenAPI examples across all three clients. Use one successful order response, one expired token case, and one validation error. If any client breaks on those shared examples, the problem shows up before release instead of after users see it.

## Mistakes that keep breaking clients

Teams rarely break clients with one huge change. They do it with tidy-looking shortcuts that pile up over time.

One common mistake is making every field optional because nobody wants to argue about what is required. That feels safe for a week, then it creates guesswork on every client. The web app sends one shape, the mobile app sends another, and the backend starts adding special cases. A payload schema should remove doubt, not preserve it.

Enum changes cause the same kind of damage. If one team changes `pending` to `in_review`, clients may not crash right away. They may show the wrong badge, skip a branch in the UI, or save bad state locally. These bugs are slow and annoying because they look like client mistakes when the API contract changed underneath them.

Error handling often breaks in a quieter way. Validation errors return one JSON shape, but server faults return something completely different. Now every client needs two parsers, two retry rules, and two ways to show messages to users. Keep the error format consistent, even when the details differ.

Auth rules drift when teams hide exceptions in tickets, chats, or private notes. One endpoint allows a service token, another silently expects a user token, and a third ignores a missing scope in staging but not in production. People cannot follow auth rules they cannot find.

The biggest process mistake is calling the approach OpenAPI-first while the spec trails behind the code. Once that happens, the spec stops being the source of truth and turns into a record of old decisions.

A quick smell test before release helps. Check that required fields are truly required, enum values did not change without notice, auth behavior matches the spec, all errors share one shape, and code and spec changed together. If a team cannot answer those points in a few minutes, client bugs are probably already on the way.

## Quick checks before release

A release should not depend on memory, old tickets, or chat history. Before anyone ships, the OpenAPI file should answer the same practical questions for every team.

Required fields should be easy to spot. A new teammate should be able to see which fields must be sent, which are optional, and which can be null without reading backend code. Protected endpoints should use a consistent auth scheme. Error responses should keep one shared structure. Examples should match real requests and responses. And one person or team should own contract approval.

Small gaps here turn into noisy bugs later. A frontend developer sends an empty string because the spec never said a field was required. A mobile team refreshes tokens the wrong way because one protected endpoint broke the pattern. These are not hard bugs. They are avoidable mismatches.

A short release review often catches them. Open the spec, pick two or three endpoints, and ask someone outside the team to describe how they would call them. If they hesitate on required fields, auth rules, or error handling, the contract still needs work.

## What to do next

Pick one endpoint that already wastes time. Choose the one that keeps breaking a mobile screen, a web form, or an internal job because teams made different guesses about the same field, token rule, or error body. That gives you a real problem to fix, not a theory exercise.

Then clean up the contract before anyone adds more code around it. For that one endpoint, settle three things first: the payload schema, the auth rules, and the error format. Review the spec with every team that sends or reads the data. Backend, frontend, mobile, QA, and anyone running imports or automations should challenge unclear parts. If one team says, "we assumed this field was optional," the spec is not done yet.

Keep the first rollout small. Update the spec, update the server, check real examples, and watch the next release closely. If bug reports drop for that endpoint, repeat the same process on the next problem area. Small wins usually change habits faster than a big process push.

If teams still cannot agree on ownership, rollout order, or how strict the contract should be, outside help can speed things up. Oleg Sotnikov does this kind of work through oleg.is as a Fractional CTO and startup advisor, with a strong focus on API design, architecture, and practical AI-first engineering workflows. That kind of review helps most when the real problem is not code quality, but team coordination.
