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.
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.
| Move | Why it comes first |
|---|---|
| 1. Read the models | They define the real data shape and often expose helpers the controllers should be calling. This is where the intended design lives. |
| 2. Transcribe the contract | For each endpoint, write down every status code the spec names. These are literal test assertions. |
| 3. Hunt the missing side effect | Find each write that changes state, and ask what else should happen. This is where the seeded bug usually is. |
| 4. Fix one, run tests | Attribution. 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 sentence | Write | Consequence |
|---|---|---|
| "Launches the movie and notifies followers of its actors" | set status to published | create a notification per following user |
| "Deletes the project and its tasks" | delete project | cascade to tasks |
| "Marks the invoice paid and emails a receipt" | set paid | send 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.
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.
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
200instead of400fails 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 onisRead— 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