Frontend broke after backend change, and no build, test or alert said so? The verdict: a missing contract, not bad prompting. Put one typed contract between the two sides, check both against it, and make CI fail on a deliberate mismatch. The tool depends on the stack: Supabase types, tRPC, a shared Zod schema, or OpenAPI with a diff step.
Frontend broke after backend change: what a typed API boundary is
A typed API boundary is a place where data crosses between two programs and the shape is declared once, with both sides checked against that declaration. A frontend breaks silently after a backend change when the shape was written by hand on each side, because a missing field reads as undefined in JavaScript and renders as nothing on the page.
A boundary is every place data crosses between two programs: the browser and your server, your server and the database, your server and a third party, and a webhook coming in. Typed means the shape lives in one declaration and both sides are checked against it, at build time by the compiler and, where the data comes from outside the repository, at run time by a parser. This is one of the engineering standards for AI-assisted teams I hold every codebase to.
Untyped boundaries fail quietly: the component reads user.full_name, gets nothing back, and the screen shows a gap where the name should be, with no error in the console. The fix depends on how the app is built, so here is my mapping of four common setups, one row each.
| Your stack | What the contract is | How each side gets its types | What fails when they drift |
|---|---|---|---|
| A Lovable or Bolt app talking straight to Supabase | The database schema, as migrations | The client uses types generated from the schema (supabase gen types) | A regenerate-and-compare step fails the pull request |
| One TypeScript repository with its own API routes (Next.js, for example) | A shared schema module, or a tRPC router | Both the route and the client import it | The typecheck fails |
| A separate backend in another language (FastAPI, for example) | An OpenAPI file the backend generates from its code | The frontend generates client types from that file | The spec diff and the typecheck fail |
| Any third party (Stripe, an email provider, an AI API) | The provider’s SDK types plus a schema you parse the response with | SDK types at build time, a parse at run time | The parser throws, with the payload logged |
What is an API contract, and what the Swagger API file has to do with it
An API contract is the machine-readable statement of what each endpoint accepts and returns: paths, parameters, schemas, status codes and which fields may be null. OpenAPI is the open standard for writing it, as a JSON or YAML document, and it was originally based on the Swagger Specification.
A full contract lists, per endpoint, the path, the method, the parameters, the request and response schemas, the status codes, and which fields are required and which may be null. I separate the two words like this: the specification is the file, and the contract is the promise that the running API matches it. The OpenAPI Specification, version 3.2.1 (dated 10 September 2026), “defines a standard, programming language-agnostic interface description for HTTP APIs”, and a conforming document “may be represented either in JSON or YAML format”.
The name history trips people up. The OpenAPI Initiative, which works as an open governance structure under the Linux Foundation, says “The OpenAPI Specification was originally based on the Swagger Specification, donated by SmartBear Software.” Swagger today is SmartBear’s family of tools around that format: Swagger Editor, Swagger Codegen, and Swagger UI, which renders Swagger API documentation from the file. SmartBear’s own page says the Swagger Specification “has since been renamed to the OpenAPI Specification”. A Swagger API, in everyday use, is simply an API that publishes such a file.
For this page the point is narrower. The documentation is a by-product; the contract is the file, and it only protects you if it is generated from the code or checked against it in CI. A file someone typed once and never updated is worse than none, because people trust it. Which portal renders the docs is a separate decision, made in the API documentation tool comparison you run when picking one; the contract comes first.
What goes wrong without it
Here are seven backend changes and what each one produces in the browser. The rows are my reading of how a typical React or Next.js frontend behaves when nothing ties it to the backend’s shape; three cells rest on React, MDN and Stripe documentation.
| The backend change | What the user sees | Why nothing warned you |
|---|---|---|
A field renamed, full_name to name | A blank where the name was | The old field reads as undefined, and the hand-written type still lists it |
| A field made nullable | A white screen when the code calls .map on null | The error is thrown during rendering, and React, by default, removes the UI from the screen |
| A number that became a string | A total that shows two prices run together | + with a string operand joins text instead of adding |
| An array wrapped in an object for pagination | An empty list, or a crash if the code calls .map on it | The fetch result was typed as an array and never checked |
| An enum value added | A status badge that falls through to its default, or shows nothing | Stripe, for one, documents that it can add new values to an open enum as a backward-compatible change |
A column dropped while the client uses select('*') | Nothing, until a component reads the column and draws a blank | * asks for whatever columns exist, so the query still succeeds |
An error shape changed, error to message | A toast that prints undefined | The handler reads a key that no longer exists |
Why nothing warned you, in almost every row: the response type was written by hand on both sides, or the fetch result was cast with as. The TypeScript handbook on type assertions is blunt about what a cast does at run time: “type assertions are removed by the compiler and won’t affect the runtime behavior of your code”. Strict compiler settings catch more mistakes inside the code, and how to enable TypeScript strict mode is worth doing first, but no setting sees through a cast to the server’s real reply. If a coding agent made the backend change, the mechanism is the one behind why AI edits break code outside the requested feature; here the fix is the contract.
Picture a founder who asks the coding agent to format prices on the pricing endpoint, and in that session the agent changes price from a number to a string such as "12.50". The frontend’s response type was written by hand and the fetch result cast with as, which does nothing at run time, so the build stays green; then the cart adds two prices, and as MDN on the + operator puts it, “If one side is a string, the other operand is also converted to a string and they are concatenated”, so the total shows the two prices run together. The cast switched the compiler off at the one boundary that changed, and a generated or shared type would have failed the build instead of the cart. Uncoordinated schema changes can break clients silently.
My June and July 2026 audits covered 21 third-party apps: a deep set of 11 public vibe-coded apps, audited exhaustively across all 12 pillars, and a held-out set of 10 disjoint apps, audited blind. At least 18 of the 21 third-party apps had no working test anywhere: 17 with literally none, plus a retail POS whose checkout “test suite” never executed the actual checkout code. Those 21 are apps I selected for audit, not a random sample, so the count describes that set and is not a rate for AI-built apps in general. In the same audits, at least 17 of the 21 had no deploy gate, and so did all 5 of my own apps: every push ships straight to production with nothing checking it first.
How to do it on Supabase, a TypeScript monorepo, and a separate backend
Typed boundaries are built from one contract per boundary: generated database types on Supabase, a shared schema or tRPC inside one TypeScript repository, and an OpenAPI file with generated client types for a separate backend. Each gets a CI step that fails when the two sides drift.
Find your row in the stack table above and do that one first. The third-party section and the change procedure at the end apply to every stack.
Supabase: generated database types, regenerated in CI
The Supabase CLI generates TypeScript types from your database schema. Supabase’s guide to generating types prints npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > database.types.ts for a hosted project, and the same command with --local for the local database. You then create the client as createClient<Database>(...), so every .from() call knows its table’s columns, and you commit the generated file.
Supabase’s docs also show a GitHub Action that runs on a schedule, regenerates the types from the hosted project and commits any change every night. That keeps the file fresh, but it reacts after the fact. My rule for pull requests is stricter: rebuild the local database from the migrations, regenerate, and fail if the file differs from the committed one. The local stack “runs in Docker containers”, so the CI job needs a runner with Docker.
# Pull-request job: rebuild from migrations, regenerate, compare
npx supabase start # local stack (Docker)
npx supabase db reset # reapplies supabase/migrations
npx supabase gen types typescript --local > database.types.ts
git diff --exit-code database.types.ts # exit 1 = committed types are stale
That turns a migration committed without its regenerated types into a red pull request. Generate the committed copy with the same --local command, so both files come from one source. The check has a blind spot: a column changed in the hosted dashboard is not in the migrations, so this job cannot see it. That drift only shows when types are generated from the hosted project with --project-id, which is what Supabase’s scheduled Action runs. The lasting fix is my rule that every schema change is a migration.
Two smaller habits. Once the client is typed, code that reads a dropped column stops compiling after the next regeneration, because the row type is “the data expected from .select()”; I still name the columns in select() rather than *, so the query says what the screen depends on. And check views: the docs warn that “a view’s column may show up as nullable when you expect it to be not null”.
One TypeScript repository: tRPC, or a shared Zod schema
When the frontend and the backend live in one repository, the contract is a module both sides import. tRPC says it lets you “build & consume fully typesafe APIs without schemas or code generation”; it relies on TypeScript’s own type inference and catches problems at build time. Without tRPC, Zod (“TypeScript-first schema validation with static type inference”) declares the shape once, z.infer gives the static type, and the same schema parses the response on the client and the request on the server.
// shared/price.ts: the contract, imported by the route and the client
import * as z from "zod";
export const Price = z.object({
id: z.string(),
amount: z.number(), // a string here fails the parse
currency: z.string(),
});
export type Price = z.infer<typeof Price>;
// route: the reply must match the type, or the typecheck fails
const reply: Price = { id, amount, currency };
// client: parse the reply, never cast it
const price = Price.parse(await res.json()); // throws ZodError on a mismatch
The rule that makes it hold is mine: no hand-written duplicate of a response type anywhere, and no as on a fetch result. Next.js server actions are typed across the call by the compiler, but Next.js says an exported action “is reachable via a direct POST request, not just through your application’s UI”, so its input still needs input validation for a web app on the server.
A separate backend: generate the OpenAPI file, generate the client types, run an openapi diff in CI
An openapi diff compares two versions of an OpenAPI file, the pull request’s and the main branch’s, and reports which changes break existing clients: a removed field, a changed type, a new required parameter. Run in CI with a fail-on-incompatible setting, it stops a breaking change before any client meets it.
Three steps, in this order. First, the backend framework emits the file from its own route and model definitions. FastAPI’s features page puts OpenAPI at the base of the framework, and FastAPI “automatically generates a JSON (schema) with the descriptions of all your API” at /openapi.json, so the file cannot drift from the code. Second, the frontend generates its types from that file with openapi-typescript, which can “Convert OpenAPI 3.0/3.1 schemas to TypeScript types and create type-safe fetching”; you run npx openapi-typescript with the schema and an -o output path, and the typecheck fails when a field the frontend uses disappears.
Third, a diff step compares the pull request’s file with the main branch’s. The OpenAPITools openapi-diff project compares “two OpenAPI specifications (3.x)”, renders the difference as HTML, Markdown or JSON, and its --fail-on-incompatible option will “Fail only if API changes broke backward compatibility”. Check the versions before you wire it in: the same README’s feature list says “Supports OpenAPI spec v3.0.”, while FastAPI’s default OpenAPI version is “the latest: 3.1.0”. So look at the openapi field of the file your backend emits and pick a diff tool that reads it. oasdiff is a second open-source option, and its README lists OpenAPI 3.1 and 3.2 support. Whichever you pick, the deliberate mismatch in the verify section below is what proves the gate works on your file.
What counts as breaking is partly a judgment call, so here is my own classification for a frontend that already ships.
| The change | Breaks the frontend that already ships? |
|---|---|
| Removing a response field the frontend reads | Yes |
| Removing an endpoint | Yes |
| Renaming a field or a path | Yes |
| Changing a field’s type, such as number to string | Yes |
| Making an optional request field required | Yes |
| Removing a value the API used to accept in a request enum | Yes |
| Adding a value to a response enum | No, but a switch with no default branch has nothing to show for the new value |
| Adding an optional field or a new endpoint | No |
The diff is only as good as the file, which is why step one matters most: a file generated from the code is the only kind I trust a diff of.
API contract testing: proving the frontend and the backend still agree
API contract testing checks each side against the contract separately instead of running both together. It comes in two kinds: schema-based, where the API’s tests validate real responses against the specification, and consumer-driven, where the frontend’s recorded expectations are replayed in the backend’s pipeline. Pact, an open-source tool, does the second.
Pact’s documentation defines contract testing as “a technique for testing an integration point by checking each application in isolation to ensure the messages it sends or receives conform to a shared understanding that is documented in a ‘contract’”. The two kinds are my grouping. Schema-based testing proves the file true: the backend’s own test suite calls each endpoint and validates the real response against the OpenAPI file. Consumer-driven testing works from the other end: Pact calls itself “a code-first consumer-driven contract testing tool”, the contract “is generated during the execution of the automated consumer tests”, and the backend’s pipeline replays it.
When is each worth its cost? Pact’s own line is that contract testing “is immediately applicable anywhere where you have two services that need to communicate - such as an API client and a web front-end”, and that it “really shines in an environment with many services”. My reading for a small team: shared types plus schema-based checks serve one repository well, and consumer-driven contracts pay off when the two sides deploy separately or belong to different people. Neither replaces an end-to-end test of the money path.
Third-party and webhook boundaries: parse, do not trust
A provider can change a response without telling your compiler. My rule: parse the fields you use with a schema at the point of entry, and fail loudly with the payload logged, so the error names the field instead of a blank screen naming nothing. Pin the provider’s API version where it offers one and upgrade on purpose. Stripe’s API versioning is the model: “Requests made with curl use your Stripe account’s default API version (controlled in Workbench) unless you override it by setting the Stripe-Version header”, and Stripe advises you “use API versioning to test a new API version before committing to an upgrade”. Stripe also says “Webhook events also use your account’s default API version unless you set an API version during endpoint creation”, so webhook payloads get the same parse, after the signature check. Hostile input is a different threat and belongs to the server-side validation above. The security side of trusting a provider’s reply, including fetched URLs, is check 9 of the API security checklist.
Changing a contract on purpose: add, migrate, remove
Contracts do change. This is my procedure for changing one without breaking clients that are still out there.
- 01 Add the new field or endpoint beside the old one; change nothing that exists yet.
- 02 Ship the backend, so both shapes are served.
- 03 Move the frontend to the new shape and ship it.
- 04 Wait out old clients: browser tabs left open for days, cached bundles, a mobile build that cannot be recalled.
- 05 Remove the old shape in a separate change that the diff flags and a person approves.
A breaking change the diff reports is allowed when it is that last step and the pull request says so in its description.
How to verify it: an API contract review checklist and one deliberate mismatch
Typed API boundaries are verified with one deliberate mismatch per boundary: rename or retype a field on the backend only, open a pull request, and confirm the typecheck, the spec diff or the contract test fails and names the field. Keep the failing and the passing runs.
Start with the review. This API contract review checklist has eight items, and each one names the evidence that settles it, so a reviewer can fail an item instead of nodding at it.
- 01 Every boundary in the app is listed: browser to server, server to database, each third party, each webhook. Evidence: the list.
- 02 Each boundary has a named contract. Evidence: the file or module named next to each entry.
- 03 No response type is written by hand in two places. Evidence: a search for the type name returns one definition.
- 04 No
asoranyon data crossing a boundary. Evidence: a search forason fetch results comes back empty. - 05 Where the stack generates type files, they are committed and regenerated in CI. Evidence: the workflow file.
- 06 The spec or schema is produced from code, not typed separately. Evidence: the generate command in the build.
- 07 Breaking changes fail the pull request. Evidence: a red run on record.
- 08 Third-party responses are parsed before use. Evidence: the schema at each entry point.
Then prove the checks bite, with four steps.
- On a branch, rename one field the frontend uses, or change its type, on the backend only.
- Open a pull request and confirm the typecheck, the diff or the contract test fails and names the field.
- Revert the change and confirm the run goes green.
- Repeat once per boundary kind; for a third party, rename the field in a saved sample payload that a test feeds to your parser.
The evidence to keep is the failing run and the passing run, each with its date. The pipeline those runs live in is its own subject, CI/CD best practices. Reviewing the same boundaries for security belongs on the secure code review checklist. The boundary list is also one chapter of a recorded tour of the code, which is the short answer to what is a codebase walkthrough. A contract review checklist for legal agreements is a different thing altogether; this one is for code.
In the Production Hardening Sprint, deliverable 10.3 is verified this way: Introduce a deliberate contract mismatch and confirm type or contract checks catch it.
Where the sprint does this
Deliverable 10.3 of the Production Hardening Sprint, typed API boundaries, is to define types at every API boundary and share contracts between frontend and backend using the stack’s appropriate tooling, for the reason the price scenario above shows. Its neighbors, in their own words: 10.6 is to enable TypeScript strict mode and resolve errors in TypeScript applications, with an equivalent strict-checking approach for other supported stacks ; 7.3 is to run linting, type checks, builds, and tests on every pull request ; and 3.1 is to validate every data-writing endpoint with explicit schemas and safe input/output handling, including injection defenses. All of it lands in the production readiness report, deliverable 13.1, which is verified this way: Account for all 123 IDs; keep failures visible until resolved and explain genuine non-applicable items. Outside this sprint: building new product features or modules, completing unfinished core features or business workflows, and rebuilding core functionality that does not yet perform its intended job. Your app’s current framework and hosting setup are our starting point, and we refactor or replace components where the production work requires it. Every deliverable is listed in the published scope.
Common questions about API contracts and Swagger
Is OpenAPI YAML or JSON?
Either. The specification lets the same OpenAPI document be written in JSON or in YAML, and tools such as openapi-typescript accept either as input. My reading: YAML is common for files people edit, JSON for files a framework generates; FastAPI, for one, serves JSON.
Is FastAPI based on OpenAPI?
Yes. FastAPI lists “OpenAPI for API creation” under the open standards it is based on, and it serves interactive docs, “2 included by default”: Swagger UI and ReDoc.
Is Swagger a free tool?
The core Swagger tools are free and open source: the Swagger UI repository says it “is licensed under Apache 2.0 license”. SmartBear also sells paid products beside them, and its page refers to “Swagger open source and pro tools”.
Is Swagger like Postman?
Partly. Both let you send requests to an API and see the replies, but Swagger’s tools center on the OpenAPI file, while Postman’s docs describe sending requests, grouping them “in collections”, writing API tests and designing an API’s structure in Spec Hub.
Can Swagger automatically generate API documentation?
Yes, from an OpenAPI file: Swagger UI is described as a way to “Automatically generate documentation from your OpenAPI definition”. The file itself comes from your framework (FastAPI serves Swagger UI by default) or from a generator. Choosing a documentation tool is a separate decision from keeping the contract true.
Owning an app means being able to run it, change it and recover it without guessing. The sprint below leaves you with the runbooks and documentation to do that.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase