How to Review an API Design With AI Before You Ship It
Code you can refactor on a quiet afternoon. A field you shipped is somebody else's dependency — you don't fix it, you version it. What an hour of contract review buys: method semantics, a truthful error model, retries, authorization, and compatibility you didn't mean to promise.
Build an API Review PromptCode you can refactor. A shipped field is somebody else's dependency.
Internals are forgiving. Pick the wrong abstraction, rename the wrong thing, put the logic in the wrong module — you find out, you fix it, nobody outside the team ever knows it happened. That forgiveness is why we're relaxed about getting code slightly wrong the first time: the cost of being wrong is an afternoon. An API contract runs on completely different economics. The moment someone builds against `"role": "admin"` sitting at the top level of your request body, that field is holding up software you don't own, can't see, and can't deploy. You don't fix it. You version it, or you deprecate it and wait a year, or you keep it forever and write a paragraph in the docs explaining why it's still there.
Which makes the review before implementation the cheapest one you will ever run, and the one most likely to get skipped — because at that point the API isn't real. There's no failing test, no bug report, nothing to react to. There's a document, or a schema, or a table of endpoints somebody sketched, and it all looks fine, because a contract's mistakes don't look like mistakes. They look like reasonable choices that turn out to have promised something. An AI genuinely helps here: point it at the whole contract and it finds the inconsistency across twelve endpoints that a human reading them one at a time won't. But be clear about what's happening — it reads what you paste and nothing else. No repository, no API gateway, no schema registry, no running endpoint; it doesn't call anything, so it can't tell you what your API *does*, only what your contract *says*. It isn't a security scan. What comes back is a set of candidate findings, some of which will be conventions dressed up as problems — and a convention is not a requirement. The API's owner decides which findings are real and which are the cost of doing business, and nothing here certifies that a design is RESTful, secure, backward-compatible, or ready for production.
A contract needs a different review than code does
It's tempting to treat this as code review with the code missing, and that framing quietly loses most of the value. A code review asks whether an implementation is correct, safe, and maintainable — questions with answers you can check against the thing in front of you. A contract review asks an odder one: what has this design *committed us to*, and what does it make impossible later? There's no implementation to be right or wrong about, and the failures aren't bugs. They're promises.
So the lens shifts. Instead of "does this work," you're asking who is going to call this and what they'll do when it misbehaves; whether the twelfth endpoint shapes its errors like the first eleven; whether the thing that happens on a retry is a decision someone made or a decision nobody made; and whether this shape can grow without a version bump. None of those are visible in a single endpoint read on its own — they're properties of the surface as a whole, which is exactly why one pass over the entire contract finds what endpoint-by-endpoint design never surfaces. The other shift is about authority, and it changes what the output should look like. A design review mostly produces *arguments* rather than defects: this is inconsistent with that, this will cost you later, this promises something you may not want to promise. Someone with product context has to weigh each one. The review's job is to make the choice visible and priced, not to make it.
Step 1: Say who's calling, and what they do when it breaks
A contract can only be judged against what it's for, so the review starts with the consumer rather than the endpoint list. Who calls this — your own front end, a partner's backend, a mobile client you can't force-update, a script somebody wrote once? What are they trying to accomplish, in whole tasks rather than individual calls? And the question that does the most work: what do they do when a call fails? A caller that retries automatically on a dropped connection turns "we didn't think about idempotency" from a design nit into duplicate charges. A mobile client you can't update turns every breaking change into a two-year support commitment. Neither is visible in the contract; both change what the findings mean.
Give the model that context explicitly, along with the constraints it can't infer — what already exists that this has to stay consistent with, what you've already promised anyone, what you can't change because it shipped last quarter. Without it, the review defaults to generic API convention, and generic convention is where the false findings come from: it'll flag a field name against a naming rule you never adopted, while missing that the retry behavior is going to bill someone twice. The difference between a review worth reading and a REST style lecture is almost entirely in how much of this you supply.
Step 2: Read the nouns and the verbs
Now go at the surface itself, starting with its shape rather than its details. Are the resources actually the things in your domain, or has an implementation detail become a URL? Is each endpoint doing one identifiable thing to one identifiable resource? A path with a verb in it — `/inviteUsers`, `/processPayment` — is usually a sign that a resource hasn't been named yet, and the tell is that its siblings are hard to name: once invitations are a resource, listing and revoking them have obvious endpoints, and while it's an action they don't.
Then the methods, which carry meaning that clients and infrastructure both rely on. `GET` is safe and cacheable, and a `GET` with side effects will be broken eventually by something you don't control — a prefetcher, a proxy, a crawler. `PUT` replaces and `PATCH` modifies, and a `PATCH` that silently clears the fields you left out is a data-loss bug wearing a spec-compliant status code. `POST` is the obvious one that isn't safe to repeat, which is what makes it the interesting case in Step 4. Check consistency while you're here, because this is where reading the whole surface at once earns its keep: names singular in one place and plural in another, IDs in the path here and the body there, a timestamp that's ISO in one response and epoch in another. Each is trivial alone and collectively they're the reason your API feels like four APIs. Ask the model to work across every endpoint and list the inconsistencies with their locations — the mechanical comparison is exactly what it's better at than you are at 5pm.
Step 3: Design the failures, not just the successes
Most contracts are written for the happy path and improvise the rest, which is backwards: consumers spend their difficult hours in your error responses, not your success ones. Start with whether the status code tells the truth. A response that says `200` while reporting that most of the work failed is the classic version — every generic client, retry wrapper and dashboard reads the status line, so a failure visible only to someone who parses the body and knows what to look for is invisible in practice. Partial success needs a deliberate answer: either the operation is atomic and a failure means nothing happened, or it's genuinely per-item and the response says so structurally. Both are defensible. Picking neither, silently, is not — and it's worth noticing that this is a decision, not a defect, which changes what the review should do with it.
Then the error envelope, checked across the whole surface rather than per endpoint. If one endpoint returns `{"error": "already a member"}` and the rest return a structured list with codes, every consumer writes two parsers and one of them gets it wrong. Errors people handle programmatically need stable machine-readable codes, not prose — prose is for humans and changes when someone improves the wording. And check the distinctions that get flattened: `401` means we don't know who you are, `403` means we know and you may not do this, and collapsing them into one is a debugging tax on every consumer forever. A `400` that's really an authorization failure is the same tax in a different currency — it sends the caller off to debug a payload that was fine all along. This is also the one place where the clearest answer and the safest one can diverge: some APIs deliberately return `404` instead of `403` so the response doesn't confirm that the resource exists. That's a real trade, and it should be a decision rather than an accident. What the model is good at here is the sweep — every endpoint, every status, every error shape, side by side. What it can't tell you is which distinctions your consumers care about.
Step 4: Ask what the second attempt does
The question almost nothing in a contract answers, and the one that produces the most expensive surprises: what happens when the same request arrives twice? It will. Networks drop connections after the server has already done the work, clients retry, users double-click, queues redeliver. So for every operation that changes something, the design has to say whether a repeat is harmless — and if it isn't, the contract needs a mechanism. Usually that's a client-supplied idempotency key, and the mechanism isn't just the header: it's the replay window, what a replay returns, and what happens when the same key arrives with a different body. Leave those unstated and you've named a header rather than defined a behavior. This is not theoretical; it's the difference between a retry that's free and a retry that emails a real person twice.
Pagination belongs in the same conversation, because it's the other thing every list endpoint needs and half of them bolt on later. Decide it up front and consistently — offsets or cursors, the default page size, the maximum, how the client knows there's more — because retrofitting pagination onto an endpoint that returned a bare array is a breaking change, and "nobody has enough rows yet" is a statement about today. Same for any collection operation: if there's no stated maximum, someone sends ten thousand items and finds out what happens at the same time you do. Ask what the caller does with the response when something goes wrong halfway through, and most of the remaining questions answer themselves.
Step 5: Rank the findings by what they cost after you ship
A flat list of thirty observations is not a review — it's homework, and it gets skimmed. Findings need severity, and the right axis isn't how wrong something is, it's what it costs to fix once it's live. Something a consumer can't work around and you can't change without a version bump belongs at the top, whatever its size. A naming inconsistency is real, worth fixing, and belongs at the bottom, because it's cheap now and cheap-ish later. Every finding needs its evidence — which endpoint, which field — the impact in terms of who it hurts, and a concrete recommendation, because "consider improving the error model" is not a finding, it's a mood.
This is also where compatibility gets its own pass, since it's the dimension that converts today's shrug into next year's problem. Look at each shape and ask what happens when it needs to grow: an array of strings where a per-item option might be needed later is a request body you'll be supporting in two shapes forever, or a version bump — while an array of objects costs a little verbosity today and absorbs the field for free. Ask what's required that needn't be, since a required field is one every client supplies forever and the day you relax it you owe it a default nobody has chosen. Then separate the findings from the decisions, because they need different endings. "The error envelope here disagrees with the rest of the API" is a finding — it has a right answer. "Should re-inviting an existing member be an error or a silent no-op" is a decision, with a real argument on both sides, and the review's job is to surface it and hand it to whoever owns the product. The Code Review Prompt Generator is the tool for building a review prompt in this shape, and two of its details are what make it usable here: pasting code is optional — the objective is the only field it requires — and the language can be set to Any, which drops the language pin from the prompt it emits.
Common mistakes
Where an API design review goes wrong, or fails to happen at all:
- Reviewing the spec and not the examples. A schema says a field is a string; the example shows what anyone actually sends. Most contradictions between the two are the design being undecided, and only the examples show it.
- Reviewing after the clients are generated. Once an SDK is published, a contract change isn't a contract change — it's a release, a migration guide, and a support thread. The review has to land while the answer is still free.
- Treating "we'll version it" as an answer. Versioning is a mechanism for changes you couldn't foresee, not a license to skip the ones you can. Every v2 is two surfaces to support, and the reason for it is usually visible in v1's review.
- Deferring pagination, filtering, and limits to later. Every one of them is additive on day one and breaking on day two, because the endpoint that returns a bare array has already promised to return a bare array.
- Confusing authentication with authorization. Knowing who someone is settles nothing about what they may do, and a privileged field accepted from any authenticated caller is where escalation gets designed in rather than exploited.
- Letting the review settle the product questions. When it resolves "error or no-op" by picking one, the decision has been made by whoever wrote the prompt, and nobody who owns the outcome ever saw it.
- Reading the findings as a verdict. They're candidates, some of them wrong — NewPrompt doesn't call your endpoints, and the model can't tell a deliberate exception from a mistake.
A batch invite endpoint, before anyone builds it
One endpoint, drafted the way endpoints usually get drafted, and what a review with the consumer in hand actually finds.
A drafted batch-invite endpoint, reviewed against the consumer that retries it: seven findings priced by what each costs once clients depend on them, and the two questions the review deliberately refuses to answerTHE CONTRACT AS DRAFTED:
POST /organizations/{id}/inviteUsers
Authorization: Bearer <token>
{ "emails": ["a@x.com", "b@x.com", "c@x.com"], "role": "admin" }
-> 200 OK
{ "results": [
{ "email": "a@x.com", "status": "invited" },
{ "email": "b@x.com", "status": "failed",
"reason": "already a member" },
{ "email": "c@x.com", "status": "failed",
"reason": "invalid email" } ] }
THE CONSUMER (without this, none of the findings below exist):
the admin UI invites 1-50 people at once, shows a per-person result,
and retries the whole request when the connection drops.
that last clause is what breaks this contract.
HOUSE CONVENTION IT HAS TO LIVE WITH:
every other endpoint returns { "errors": [ { code, message } ] }
FINDINGS - ranked by what each costs once it's live:
[HIGH] a retry invites people twice
the consumer retries on a dropped connection, by design. no
idempotency key exists, so invites that already succeeded are sent
again -- a second email to a real person. the consumer's own
documented behavior guarantees this happens.
-> accept an Idempotency-Key header, and define the behavior, not
just the header: the replay window, what a replay returns, and
what happens when the same key arrives with a different body.
[HIGH] authorization is undefined at the field level
role: "admin" is accepted from any authenticated caller. the token
settles authentication; nothing here settles whether THIS caller
may grant admin. that's escalation via a field nobody reviewed.
-> write the grant matrix down -- which caller roles may grant
which target roles -- and reject with 403, not 400: the request
is well-formed, the caller isn't allowed.
[HIGH] 200 for a request that mostly failed
two of three failed; the status line says OK. generic clients,
retry wrappers and dashboards read the status line.
-> this one is a DECISION, not a defect (see the bottom). the two
honest options: atomic -- a failure means nothing was created,
422, and the caller retries the whole thing. or per-item -- 207
with a status per invite. be clear what 207 buys: it is still a
2xx, so a dashboard reading the status line still sees success.
what it buys is an honest signal to clients that opt into
per-item semantics. if the generic-client problem is the one
you care about, atomic is what actually solves it.
(207 is a WebDAV code borrowed by convention for JSON batches --
widespread, not spec-sanctioned. worth knowing you're borrowing.)
[MEDIUM] the shape cannot grow
a list of emails plus one shared role means per-invite roles later
are either a version bump or a body you support in two shapes
forever.
-> "invitations": [ { "email": "...", "role": "..." } ]
[MEDIUM] the batch is unbounded
nothing states 50 is the limit, so someone sends 10,000 and you
both find out together.
-> state a maximum. 422 fits (well-formed, won't process); 413 is
the conventional size answer and someone will argue for it.
either works -- pick one and say so.
[LOW] a verb in a namespace of nouns
/inviteUsers is an action; every other resource here is a noun.
the tell: "list the pending invites" has no obvious endpoint.
-> POST /organizations/{id}/invitations. listing and revoking then
name themselves.
[LOW] the error shape disagrees with the rest of the API
"reason" is free text; the house convention above is an errors
array with codes. consumers end up writing two parsers.
-> reuse the house envelope exactly. "already a member" becomes a
code, not a sentence.
THE REVISED CONTRACT (still before any code exists):
POST /organizations/{id}/invitations
Idempotency-Key: <client-generated> required
replay window: 24h
a replay returns the original response, unchanged
same key + different body -> 409
{ "invitations": [ { "email": "a@x.com", "role": "admin" } ] }
max 50 per request
grant matrix: owner -> any role | admin -> member | member -> none
-> 207, per item: { email, status, errors: [ { code, message } ] }
[owner chose per-item over atomic -- see below]
-> 403 caller may not grant the requested role
-> 409 Idempotency-Key replayed with a different body
-> 422 batch over the stated maximum
NOT THE REVIEWER'S CALL:
atomic or per-item? the review priced both and the owner picked
per-item, because the admin UI shows a per-person result. that is a
product judgment, and it is recorded here as one rather than
smuggled in as a fix.
re-inviting an existing member: an error, or a silent no-op? the
admin UI would prefer a no-op. support would prefer an error they
can see in a log. both are defensible, the answer changes the
per-item contract, and it is still open -- so it goes back to
whoever owns the invitation flow, and it gets decided before the
handler is written rather than discovered after.
Where this fits in NewPrompt
This review is the last step of a longer design sequence, and the AI API Design Workflow is that sequence: model the resources and boundaries, design the endpoints and payloads and validation, pin the contract and its versioning rule, then review the design before it's code — "while it's still cheap to change, because once clients depend on it the contract is fixed." This guide is that fourth step done properly. The workflow supplies each step's prompt and order; you run them in your own AI tool, and as its own copy puts it, the API decisions and the compatibility promises you make to clients stay yours. The mechanism is the Code Review Prompt Generator — a prompt builder that turns an objective into a review contract with criteria, severity rules, and a verdict format. It's built for code review and has no API-design mode: its scopes and focuses are written for files and diffs, so the objective you write is what points it at a contract instead. What it gives back is a prompt, not a review.
Two neighbours use the same words for different jobs, which is worth untangling. API documentation describes a contract that's already decided — it takes an endpoint that exists and produces the accurate reference someone integrates against, and getting it wrong means a consumer is misinformed. This decides the contract, before there's an endpoint to describe, and getting it wrong means a consumer is stuck. The order runs one way: review the design, build it, then document what you built. Code review is a third thing — its artifact is a diff, its question is whether the implementation is correct and safe, and it happens after this contract is settled enough to implement.
Once the contract is agreed, it becomes the thing you test against — the API Test Prompt is a copy-paste prompt for exactly that, with the contract as the subject rather than the implementation: status codes, response shapes, `401` versus `403` as separate tests, and what a duplicate of a completed request does. That's the next step rather than this one, and it needs an endpoint to point at. If you want the model reasoning about boundaries and trade-offs before it starts listing findings, the Software Architect Role Prompt is a copy-paste role prompt aimed at pressure-testing a design before it gets built, built with the Role Prompt Generator. And if the API is a new backend, the Build an API Backend with AI project is the fuller path this sits inside — its design phase pins the contract before code makes it permanent, and, as the project says plainly, it doesn't write or deploy the backend for you.
The measure of a review like this isn't how many findings it produced. It's what reaches the person who can actually settle the question. A review that quietly picks atomic over per-item has removed a product decision from the only people qualified to make it, and done it in a document nobody reads twice — which is worse than not reviewing at all, because now the choice looks considered. The findings the model is best at are the ones with right answers: the twelfth endpoint's error shape, the verb in the path, the field that can't grow. The questions worth escalating are the ones with two defensible answers and a business behind them. Get those in front of the owner while the contract is still a document, and the expensive part of this API is over before anyone writes a handler.