Booking API Endpoints
Booking GET + POST endpoint internals
Booking API Endpoints
This page details the internal logic of the GET /api/booking and POST /api/booking endpoints, covering data retrieval, validation, plan limit enforcement, and appointment conflict detection.
GET Endpoint Internals
The GET /api/booking endpoint retrieves a professional's availability and existing appointments for a given date.
It expects two query parameters: slug and date. The slug is used to look up the professional in the professionals table via eq(professionals.slug, slug) in src/app/api/booking/route.ts. If no matching professional is found, the endpoint returns a 404 response.
For the specified date, it fetches:
- Weekly availability rules from the
availabilitytable. - Existing appointments from the
appointmentstable, filtered byprofessionalId, the requested date range (usingstartOfDayandendOfDay), and excludingrejectedorcancelledstatuses.
The response includes the professional's data, their availability rules, and a list of existing appointment time slots, which the client uses to generate selectable booking options.
POST Endpoint Internals
The POST /api/booking endpoint handles new appointment creation with multiple validation and business rule checks.
Rate Limiting
Requests are rate-limited using getBookingLimiter() from src/lib/ratelimit, based on the client's IP address retrieved via getIp(req). If the limit is exceeded, the endpoint returns a 429 Too Many Requests response using the tooManyRequests() helper.
Validation
The request body is validated using zod against the bookingSchema defined in src/app/api/booking/route.ts. Required fields include slug, startsAt, clientName, clientPhone, and optional clientEmail. Invalid input results in a 400 Bad Request response with structured error details.
Plan Limits
The endpoint enforces client and monthly appointment limits based on the professional's current plan, determined by effectivePlan(professional.plan, professional.trialEndsAt).
- Client Limit: If the plan's
clientslimit is finite, the endpoint counts existing clients. If the limit is reached and the booking client is not found by phone (or email, if provided), it returns a403 Forbiddenresponse. - Monthly Appointment Limit: If the plan's
appointmentsPerMonthlimit is finite, the endpoint counts appointments in the current month (startOfMonthtoendOfMonth). If the limit is exceeded, it returns a403 Forbiddenresponse.
These checks occur before the overlap validation.
Overlap Prevention
To prevent double-booking, the endpoint performs an overlap check using a SELECT query on the appointments table:
const conflict = await db
.select({ id: appointments.id })
.from(appointments)
.where(
and(
eq(appointments.professionalId, professional.id),
notInArray(appointments.status, ["rejected", "cancelled"]),
lt(appointments.startsAt, endDate),
gt(appointments.endsAt, startDate)
)
)
.limit(1)This query checks for any non-cancelled or non-rejected appointments that overlap with the proposed [startDate, endDate] window. If a conflict is found, the endpoint returns a 409 Conflict response with a user-facing message in Portuguese.
Client Upsert
The endpoint attempts to find an existing client using the provided clientPhone or clientEmail (if present) for the professional. If no match is found, a new client is created. If a client is found but lacks an email and one is now provided, the record is updated.
Known Gaps
- Race Condition in Overlap Check: The overlap check and subsequent insert are not wrapped in a transaction with a locking strategy. This creates a theoretical race condition where two concurrent requests could both pass the
SELECTcheck and proceed to insert, resulting in a double-booking. The current implementation relies on the narrow timing window and low booking volume to avoid this issue. A stronger guarantee would require a PostgresEXCLUSION CONSTRAINTorSELECT ... FOR UPDATEwithin a transaction.
Why This Shape
The Booking API separates read (GET) and write (POST) concerns for clarity. The GET endpoint provides all necessary data for the client to render available slots, while the POST endpoint enforces business rules, validation, and conflict detection server-side. Validation with zod ensures data integrity at the boundary. Plan limits are enforced early to prevent unnecessary processing. The overlap check, while not transactionally locked, provides practical protection against double-booking under current load. This design balances correctness, maintainability, and performance for the core booking workflow.