Nobody has started this yet — be first.
Business impact
This is the worst class of bug an API can ship: the request the user made failed (they got an error back, or at least not a success), and yet their data is gone anyway. A recipe author tries to update a recipe, makes a mistake in the payload that the database rejects, gets back a failure -- and when they go look at the recipe afterward, it now has zero ingredients and zero steps. There is no error message telling them this happened, no undo, and nothing in the API's response hints at it -- the failed request looks like it just... failed, while quietly deleting everything. For anything resembling production data, this is the kind of bug that turns into an incident: support tickets from users insisting "I didn't touch it, it just disappeared," with no audit trail pointing back to the failed PUT that actually did it.
Problem
PUT /recipes/:id deletes the recipe's existing ingredients and steps before attempting to recreate them, and those deletes are not part of the same transaction as the recreate. If the recreate step then fails for any reason (e.g. the request accidentally contains two steps with the same stepNumber, which violates the @@unique([recipeId, stepNumber]) constraint in prisma/schema.prisma), the recreate rolls back -- but the deletes, having already committed as separate statements, do not roll back with it. The recipe survives (it was never itself deleted), but it's left with none of its original ingredients or steps.
Current behavior
After a PUT /recipes/:id request fails (e.g. 500 from a duplicate stepNumber), GET /recipes/:id shows the recipe with ingredients: [] and steps: [] -- both wiped out despite the request having failed.
Expected behavior
A PUT /recipes/:id request that fails for any reason must leave the recipe in exactly the state it was in before the request -- full failure, zero side effects. Either the whole update (delete old + create new) succeeds, or none of it takes effect.
Steps to reproduce
curl -X POST http://localhost:3001/recipes -H "Content-Type: application/json" -d '{ "title": "Chicken Stir Fry", "cookingTimeMinutes": 20, "servings": 4, "ingredients": [{"name": "Chicken breast", "quantity": 500, "unit": "g"}], "steps": [ {"stepNumber": 1, "instruction": "Slice the chicken."}, {"stepNumber": 2, "instruction": "Cook it."} ] }'
curl -X PUT http://localhost:3001/recipes/<id> -H "Content-Type: application/json" -d '{ "title": "Chicken Stir Fry", "cookingTimeMinutes": 20, "servings": 4, "ingredients": [{"name": "Chicken breast", "quantity": 500, "unit": "g"}], "steps": [ {"stepNumber": 1, "instruction": "First step."}, {"stepNumber": 1, "instruction": "Duplicate step number -- the DB rejects this."} ] }'
Why this matters
This is a transaction-boundary bug, not a logic bug -- every individual statement here is written correctly in isolation, and the mistake only shows up when you trace which operations are (and aren't) inside the prisma.$transaction(...) block. The delete calls use the plain prisma client; the recreate uses the tx client Prisma hands to the transaction callback. Those are two different execution contexts, and Postgres commits each one independently -- the deletes are permanent the instant they run, regardless of what happens next. This is exactly the kind of defect that's invisible from a single successful request (delete-then-recreate looks identical to a rollback-safe version when nothing fails) and only shows itself when the second half of the operation fails -- which is precisely why a dedicated test for the failure path (not just the happy path) is what catches it.
Suggested approach
Look at how POST /recipes's nested-create handler in this same file gets true all-or-nothing behavior "for free" (per the comment above it) by keeping every write inside a single Prisma call. Now look at where the prisma.$transaction(...) boundary actually starts and ends in the PUT handler relative to the two deleteMany calls -- and at which client object (prisma vs the tx argument) each of the four database calls in this handler is issued through.
Acceptance criteria
Verification
npx jest --config practice-tickets/jest.config.js practice-tickets/tests/07-put-atomicity-data-loss.test.js
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/put-atomicity-data-lossFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/put-atomicity-data-lossSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign incurl http://localhost:3001/recipes/<id>
docker-entrypoint.shnpx prisma migrate deploydocker compose up --builddocker compose down -vWithout Docker: npm install, copy .env.example to .env and point DATABASE_URL at a local PostgreSQL instance, then npm run prisma:migrate:dev and npm run dev (nodemon, port 3001) or npm start.
The project's own suite (tests/recipes.test.js) runs against a real Postgres test database, not a mock -- create one, copy .env.example to .env.test and point it at that database, then run:
npm test
Work the tickets in practice-tickets/tickets/ (01 through 07); each names one Jest test in practice-tickets/tests/, run against the same real test database via practice-tickets/jest.config.js:
npx jest --config practice-tickets/jest.config.js practice-tickets/tests/01-max-cooking-time-boundary.test.js # a single ticket
./practice-tickets/run.sh # all 7, clean pass/fail summary
Fixing tickets 02 and 04 also turns two tests in the project's own npm test suite green again -- they break as a direct, expected side effect of those bugs touching shared route code the existing suite also exercises. That's expected, not a mistake in the scaffolding.
Level 1
Fix a bug
Read existing behaviour, correct it.