This walkthrough covers Next.js Bookings Without Double Booking. A Next.js App Router application, authenticated users and a MySQL database. This example uses fixed-capacity time slots rather than arbitrary overlapping time intervals.
The video is an illustrated code walkthrough with AI narration. Code blocks are integration excerpts, not a complete tested application. Helper names such as stores, middleware and clients describe responsibilities you must implement in your existing project. No live server, payment provider or device execution is claimed.
Before you begin
Use a separate development environment, fictional data and test credentials. Record dependency versions and follow the official references below for your installed versions. Keep a working baseline and add one workflow at a time.
1. Choose a precise booking model
Begin with fixed slots and one active booking per slot. A unique database key can then enforce the capacity rule directly. Arbitrary start and end intervals are a different problem and need overlap-safe locking or another appropriate design. Store instants consistently and display them in the user's time zone. Include the resource, cancellation policy, and booking window in the requirements.
slots: id, resource_id, starts_at_utc, ends_at_utc
active_bookings: slot_id PRIMARY KEY, user_id, created_at
booking_events: id, slot_id, actor_id, action, created_at2. Return availability as information
The availability page is a snapshot, not a reservation. Another user may book between loading the page and pressing confirm. Make the interface prepared for that outcome. Return stable slot identifiers and explicit timestamps rather than asking the browser to invent the booking interval. Validate requested dates and limit the query window to avoid expensive unbounded calendar requests.
GET /api/slots?day=2026-12-01
Response: [{id, startsAt, endsAt, available}]
// Availability can change before the user submits.3. Lock and insert atomically
Use a transaction for the booking decision and its persistence. Lock the slot row, validate the current business conditions, then insert the active booking. The primary key remains the final capacity guard. Translate a duplicate-key conflict into a clear already-booked response. Always roll back on failure and release the connection. Don't send confirmation emails while holding the database lock.
BEGIN;
SELECT id FROM slots WHERE id=? FOR UPDATE;
-- Check resource access, time and booking window.
INSERT INTO active_bookings(slot_id,user_id) VALUES (?,?);
INSERT INTO booking_events(slot_id,actor_id,action)
VALUES (?, ?, "booked");
COMMIT;4. Handle conflicts in the interface
Disable repeat clicks while a booking request is pending, but don't rely on that to prevent duplicate requests. On a capacity conflict, preserve the user's context and refresh the availability list. For retries after network uncertainty, add an idempotency key tied to the authenticated user and operation. A failed response doesn't always mean the server failed to commit the booking.
if (response.status === 409) {
setMessage("That slot was just booked. Choose another.");
await refreshSlots();
return;
}5. Use one connection per transaction
A database transaction belongs to a connection. Calling independent pool queries inside the transaction block can accidentally run them on different connections. Keep every related query on the acquired connection and release it in a finally block. Test failure after each meaningful write. Consider short bounded retries for retryable database conflicts, while keeping business conflicts visible to the user.
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// All booking queries use connection, not pool.
await connection.commit();
} catch (error) {
await connection.rollback(); throw error;
} finally { connection.release(); }6. Cancel with authorization
Cancellation is a protected mutation. Check who owns the booking and whether the cancellation window remains open before releasing capacity. Use the same slot-locking discipline as booking so concurrent actions behave predictably. Preserve an event record for support and reporting rather than erasing all history. Decide what repeated cancellation requests should return and keep that behaviour idempotent.
BEGIN;
SELECT * FROM active_bookings WHERE slot_id=? FOR UPDATE;
-- Verify owner or authorized staff and cancellation window.
DELETE FROM active_bookings WHERE slot_id=?;
-- Append cancellation event.
COMMIT;7. Separate time display from storage
Store concrete instants consistently and format them in an explicit display time zone. Recurring local schedules require additional handling around daylight-saving changes; a fixed offset isn't a complete time-zone model. Include the zone in confirmation screens and messages. Test ambiguous and skipped local times if your product creates recurring availability. Avoid silently interpreting a bare date-time string in the server's local zone.
const label = new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium", timeStyle: "short",
timeZone: selectedTimeZone
}).format(new Date(slot.startsAt));Integration checks
Submit two simultaneous bookings for one slot: exactly one active booking must remain. Test retries, rollback, unauthorized cancellation, booking windows and time-zone boundaries.
These are verification instructions, not reported test results. The excerpts have not been executed as a complete integrated application. Review authentication, database schema, dependency versions and operational settings before deployment.
Official references
Explore CodeKits development services.

