Minimal Next.js app for the escape room booking take-home.
If you are reviewing the implementation, the core files to focus on are:
app/api/holds/route.tsapp/api/holds/[holdId]/route.tsapp/api/holds/[holdId]/confirm/route.tslib/booking.tsprisma/schema.prismatest/booking.test.ts
Create a .env file in the project root with:
DATABASE_URL="file:./dev.db"Then run:
npm install
npx prisma generate
npx prisma db push --force-reset
npm run db:seed
npm run devOpen http://localhost:3000.
db push --force-reset is the quickest local reset here because the schema changed a few times during the take-home and the checked-in dev.db may still reflect an older shape.
Roomstores the room catalog.Reservationstores both temporary holds and confirmed bookings for a room at a concrete start time.
The API only accepts startsAt for a reservation request. Slot length is fixed at one hour.
- It avoids pre-generating slots forever into the future.
- It keeps the schema small and easy to explain.
- It keeps rooms as reference data and reservations as the only dynamic state in the database.
- For this take-home, SQLite's single-writer behavior plus transactional reservation logic is a pragmatic way to prevent double booking without adding extra infrastructure.
This was built as a short take-home, so I intentionally kept the implementation small:
- one
Reservationtable handles hold, confirm, and release - the API only exposes the three required actions
- slot length is fixed to one hour to avoid extra scheduling complexity
- validation is intentionally minimal so the core hold lifecycle stays easy to follow
- minimal tests
With more time, I would have:
- fuller request validation for fields like email and datetime shape
- allow more dynamic time slots
- better and more complete tests
- idempotency or safer retry handling for confirm/release actions
- stronger database-backed concurrency guarantees if this moved beyond SQLite or needed to support heavier contention
I intentionally removed a separate hold table and modeled the lifecycle on a single Reservation row with statuses like HELD, CONFIRMED, and RELEASED.
That simplifies the confirm flow from "create a booking from a hold" into "update a reservation status", but it also means uniqueness for an active slot is enforced in application logic rather than with a simple database unique constraint. A plain unique key on roomId + startsAt would incorrectly block future reservations after a hold is released or its holdExpiresAt has passed.
POST /api/holdscreates a 5-minute hold for a room and start timePOST /api/holds/:holdId/confirmconfirms a hold for the same email that created itDELETE /api/holds/:holdIdreleases a hold for the same email that created it
Create a hold:
curl -X POST http://localhost:3000/api/holds \
-H "Content-Type: application/json" \
-d '{
"roomId": "1",
"email": "user@example.com",
"startsAt": "2026-04-12T10:00:00-07:00"
}'Copy the returned hold.id, then confirm it:
curl -X POST http://localhost:3000/api/holds/<holdId>/confirm \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'Or release it instead:
curl -X DELETE http://localhost:3000/api/holds/<holdId> \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'Quick sanity checks:
- an empty
POST /api/holdsshould return400 - creating the same hold twice before expiry should return
409 - confirming a released or expired hold should return
409 - confirming or releasing a hold with a different email should return
409
npm run test
I kept the automated tests intentionally small and focused on the reservation rules that matter most for this exercise.
With more time, I would add API-level integration tests that exercise the real route handlers and database together, including:
- creating a hold through
POST /api/holds - confirming a hold through
POST /api/holds/:holdId/confirm - releasing a hold through
DELETE /api/holds/:holdId - end-to-end duplicate hold / duplicate booking scenarios against a dedicated test database
I used AI during development to speed up iteration on boilerplate, refactors, help with logic, and this README.
What AI helped with:
- scaffolding and cleanup of the initial project structure
- iterating on Prisma schema shape and route organization
- tightening tests and error handling
- refining README wording and tradeoff explanations
What I made sure of:
- I kept the implementation intentionally small enough that I can explain every file and every decision in an interview
- I reviewed and simplified generated suggestions rather than pasting them in blindly
- I only kept code paths that I understand and can defend, especially around reservation lifecycle and concurrency assumptions