AiLearn AI Coding
Learn/Common Patterns Premium

API Contracts & Side Effects

Debugging a broken service — reading existing models, honoring exact status codes, and finding the write that forgot to trigger downstream work.

13 min readUpdated 2026-08-09

Most of this site's patterns are algorithmic. This one isn't. It shows up when the interview hands you a working-ish multi-file service — routes, controllers, models, a spec, and a failing test suite — and says: the feature is broken, make the tests pass.

It is now one of the most common AI-enabled interview shapes, because it is precisely where AI assistance is strong on volume and weak on judgment.

What it solves

Problems where correctness is defined by a written contract rather than a complexity bound. There is no clever algorithm to find. The difficulty is fidelity: matching an API spec exactly, and noticing the work a write operation is supposed to trigger but doesn't.

Recognition signals

  • The prompt describes a system that is misbehaving, not a function to write: "notifications aren't being sent", "the count is wrong"
  • You're given an endpoint table — method, path, request body, and per-case status codes
  • Test names read like behaviors, not algorithms: "should prevent following the same actor twice"
  • The repo has a models / controllers / routes split, and the models are more complete than the controllers
  • Some tests pass already — the scaffolding works, specific behaviors don't

The approach

Four moves, in this order. The order matters more than the code.

MoveWhy it comes first
1. Read the modelsThey define the real data shape and often expose helpers the controllers should be calling. This is where the intended design lives.
2. Transcribe the contractFor each endpoint, write down every status code the spec names. These are literal test assertions.
3. Hunt the missing side effectFind each write that changes state, and ask what else should happen. This is where the seeded bug usually is.
4. Fix one, run testsAttribution. Batched fixes produce a wall of failures you can't map back to causes.

Move 1 is the one candidates skip

The most common way to fail this problem is to write technically-correct code that ignores the codebase. If the model exposes Follow.getFollowersByActor(...), and you hand-roll the equivalent query in the controller, you may still pass — or you may not, because the helper does something you didn't replicate, like normalizing names or populating a relation.

Either way, an interviewer grading "did they read the code" has their answer. Treat existing abstractions as the intended API.

warning

When an AI tool writes a controller for you, it invents plausible database calls from the endpoint name alone. It has not read your models unless you gave them to it. Paste the model file into the prompt, or you'll get generic code that ignores the abstractions the repo already has.

Move 3 is where the bug lives

State-change endpoints are rarely broken at the state change. The status field really does flip to published. What's missing is everything that was supposed to happen because it flipped: notifications created, counters updated, caches invalidated, events emitted.

Read the endpoint's description in the spec, and separate it into the write and the consequences:

Spec sentenceWriteConsequence
"Launches the movie and notifies followers of its actors"set status to publishedcreate a notification per following user
"Deletes the project and its tasks"delete projectcascade to tasks
"Marks the invoice paid and emails a receipt"set paidsend receipt

The clause after the "and" is the bug.

What to tell the AI

Give it the models and the exact contract. Do not ask it to infer either.

Prompt — avoid
The notification tests are failing, fix the notification system.

No models, no contract, no specific behavior. You'll get a plausible rewrite that ignores the repo's existing helpers.

Prompt — good
Here are my Follow and Notification models [paste]. In the launch endpoint, after saving the movie with status 'published', iterate movie.stars; for each actor use Follow.getFollowersByActor(actorName), and for each follower call Notification.createNotification(...). Use only those two existing statics — do not write raw queries.

Models supplied, side effect stated explicitly, existing abstractions named. Now the AI is doing typing, not guessing.

What to verify

  • Every status code in the spec. A duplicate follow returning 200 instead of 400 fails the test even though the data is right. Read the table, not your instincts.
  • The side effect actually persisted. A notification object built in memory and never saved passes nothing. Confirm the write.
  • Per-recipient fan-out. "Notify followers" means one record per follower per actor, not one record total.
  • The existing helpers were used where they exist.
  • Idempotency and guards. Launching an already-launched item shouldn't re-notify everyone.
  • The read path agrees with the write path. If marking-as-read sets isRead, the count endpoint must filter on isRead — the two must reference the same field.

Don't accept a diagnosis without the trace

When a test returns a 500, read the actual stack trace before you accept any diagnosis — including your own. An AI asked "why is this 500ing?" without the trace will confidently invent a cause, and its suggested fix often removes a validation that was doing its job. The trace names the file and line. Start there.

Worked micro-example

A follow/notify service. Two endpoints, one seeded bug each.

POST /follow currently appends the actor and returns 201 unconditionally. The spec says a duplicate follow returns 400. The data layer may even have a unique index that throws — surfacing as a 500, not the 400 the test wants. Fix: check for the existing follow first and return 400 explicitly.

POST /movies/:id/launch sets status = 'published' and returns the movie. The spec says it also notifies followers of the movie's actors. Nothing in the handler touches notifications — so unread-count stays 0 and three tests fail at once. Fix: after the save, fan out over movie.stars and create one notification per follower.

Notice the shape: one bug is a contract violation, one is a missing consequence. That pairing is the standard construction of these problems.

Practice problems

Work Release Radar first — it's built on exactly this pattern, with a contract violation and a missing side effect. Then Schedulr (booking conflicts and status transitions), LinkLock (expiry rules and analytics counters), and Transcribe (job lifecycle where the consequence is a state machine).

Quick check · A launch endpoint correctly flips a record's status to 'published', but three notification tests fail. Where should you look first?

Premium

Unlock the rest of this guide

Premium unlocks every pattern deep-dive, every problem breakdown and solution, the practice sandbox, and verdict feedback on your practice runs.

  • Read the models and their helpers before any controller: the repo usually already has the abstraction you were about to reinvent.
  • The bug is rarely the state change itself — it's the side effect the state change was supposed to trigger.
  • Tests assert exact status codes. 400 vs 404 vs 409 is part of the contract, not a detail.
  • Fix one behavior, run the suite, repeat. Batched fixes make failures impossible to attribute.
  • The full article, complete and uninterrupted
  • All pattern deep-dives and problem breakdowns
  • Practice sandbox and verdict feedback