DEV Community

Jeff
Jeff

Posted on

Day 2: Designing an API with AI before the first line of code

Greenfield APIs start in one of two ways. A meeting produces a wiki page that says things like "reservation endpoint" and "some kind of webhook," and integration week becomes a negotiation about what everyone thought they agreed to. Or the team writes the contract first, argues while it is still cheap to argue, and turns that contract into something executable before the backend exists.

This is Day 2 of the the series: designing an API with an AI assistant without letting the assistant become the authority on what the API does.

Start from behavior, not from routes

The mistake I make when facing a blank spec is asking the model to "design a reservations API." That produces a textbook CRUD surface with no opinion about the business. The prompt that works describes behavior and constraints — the parts humans actually agree or disagree about:

Members reserve a time slot. A duplicate reservation for the same member and slot returns 409. A hold is created in a held state, expires after 15 minutes without payment, and releases the slot. Confirmation emits an event the client subscribes to. Money is integer cents. Every mutating request accepts an Idempotency-Key.

Two things happen when you start here. The endpoint set shrinks (we merged hold and confirm into one resource at the plan stage), and the genuinely contentious decisions show up immediately: state transitions, expiry semantics, idempotency, money.

Plan first, patches second

A good spec workflow does not dump forty endpoints into the document after one prompt. It returns a plan — the proposed operations, the state machine, the schemas — and waits. Plans are cheap to reject; a generated file full of endpoints feels done when it is merely large.

Once the plan is accepted, each change lands as a patch card: a rendered diff against the current document, apply or reject, with a revision log behind it. Treat it exactly like a pull request, because it is one. The senior engineer's job shifts from typing schemas to reading contract diffs, which is a much better use of the time.

The rule that keeps this safe: manual edits always win. When a human and the model both touch an operation, the human version survives and the assistant continues from the new state. Without that guarantee, nobody dares touch the document and the AI becomes a single point of failure.

Force the boring decisions into the open

The integration bugs I see in code review are almost never exotic. They are the same five decisions nobody settled:

  • Money. Integer cents plus an ISO 4217 currency field. Floats are banned from the contract.
  • Idempotency. State-changing requests take Idempotency-Key; a replayed request returns the original resource instead of creating a second one.
  • Errors. One envelope — error.code, error.message, optional error.details — so clients write a single handler instead of parsing twelve string formats.
  • Pagination. Cursor-based for anything that grows, with an opaque cursor. No page versus pageSize argument in month three.
  • Concurrency. A version field on contended resources; stale writes get a 409 carrying the current state.

The model will happily violate all five if you let it. It suggested dollar floats on the first draft of the reservations spec; the diff caught it in ten seconds. That is the correct division of labor — the machine drafts, the diff reveals, the human decides.

The agreed hold operation ended up small and specific:

/holds:
  post:
    summary: Create a 15-minute hold on a slot
    parameters:
      - in: header
        name: Idempotency-Key
        required: true
        schema: { type: string }
    requestBody:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [slotId]
            properties:
              slotId: { type: string, format: uuid }
    responses:
      '201':
        description: Hold created; confirm within 15 minutes
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Hold' }
      '409':
        description: Slot already held or booked
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Error' }
Enter fullscreen mode Exit fullscreen mode

"Done" is executable before the backend exists

Here is the payoff, and the part that changes how the team works. With no service deployed, the spec can already:

  1. answer real HTTP through a mock built from its examples (Day 3), and
  2. pass or fail scenario tests that encode the behavior from the original prompt (Day 4).

For the reservations service, the definition of done before implementation started was: create a hold and capture the id; replay the same request with the same idempotency key and get the same hold back; attempt a second hold and get a 409 in the shared error envelope; subscribe to the confirmation stream and see the event.

The mobile team builds against that on day one. When the real service lands, the environment pointer changes from mock to staging and the same scenarios run unchanged. If they pass, the implementation matches what the room agreed to — not what someone remembered.

Where AI design goes wrong

Three failure modes, all avoidable:

  1. Accepting generated volume as progress. Forty unreviewed endpoints are forty unverified assumptions. Review the plan, then review every diff.
  2. Letting the model invent business rules. It has never met your compliance officer and does not know that holds cannot exceed fifteen minutes in your jurisdiction. Constraints go in the prompt; the model drafts within them.
  3. Skipping the executable step. A spec that is only ever read is a wiki with better syntax. Mocks and scenarios are what turn it into a contract the build can be measured against.

The deeper question — when AI writes the system, who defines what done means — is worth taking seriously before the first sprint; I wrote about it here. The workflow I use lives in the Powerduck workspace, where the AI edits the local spec as diffs and the mock and scenarios derive from the same file; the same loop is straightforward to assemble from any OpenAPI editor plus a mock server and a test runner.

Tomorrow, Day 3: the mock server. Not a folder of hand-written JSON that rots, but one generated from the spec that just unblocked the frontend.

Top comments (0)