Introduction
Booking a consultation should be straightforward for the person making the appointment. Behind the scenes, however, an automated booking system has to coordinate several moving parts: collecting customer information, checking availability, scheduling an event, generating a meeting link, saving the booking, and notifying the people involved.
I encountered these challenges while developing the booking system for JOPAD Advisory. I built the system from scratch, using Node.js and Express for the backend, MongoDB for storing bookings, Google Calendar for scheduling, Google Meet for virtual meetings, and Resend for email notifications.
In this article, I'll walk through how I implemented the booking workflow, how the different services work together, and some of the engineering considerations that come with connecting external APIs to a backend application.
1. The Architecture
The backend coordinates the booking process. Each service has a specific responsibility:
- Node.js and Express: Handle booking requests and coordinate the workflow.
- MongoDB and Mongoose: Store booking details and provide access to existing appointments.
- Google Calendar API: Create calendar events for scheduled sessions.
- Google Meet: Generate a virtual meeting link through the Calendar event's conference configuration.
- Airtable: Receive a secondary record of the booking.
- Resend: Power the email notification workflow.
The booking process follows this sequence:
- A customer submits a booking request through the website.
- The Express endpoint validates the submitted fields.
- The backend calculates the appointment's start and end times.
- Existing bookings are checked for overlapping time intervals.
- Google Calendar creates an event and requests a Google Meet conference.
- The booking is saved to MongoDB with its meeting link.
- The backend attempts to send a secondary record to Airtable.
- Booking notification emails are triggered.
This design brings several services together behind a single booking endpoint, giving the frontend one place to submit the request.
2. Building the Booking Endpoint
I used an Express POST endpoint to receive booking requests. The request contains the appointment time, duration, customer's contact details, session description, and other booking information.
The first step is to reject incomplete requests before performing external API operations.
A simplified version of the validation looks like this:
router.post("/book", async (req, res) => {
const {
time,
duration,
email,
firstName,
lastName,
description
} = req.body;
if (
!time ||
!duration ||
!email ||
!firstName ||
!lastName ||
!description
) {
return res.status(400).json({
message: "Missing required fields"
});
}
// Continue with availability checks
// and booking creation.
});
This is a simplified illustration of the validation logic in my implementation, not a complete replacement for the original endpoint.
The actual endpoint validates additional fields, including the selected date, day, time range, phone number, and referral information.
The important principle is to validate the data required by the booking workflow before creating external resources or writing records.
For a production system, request validation should also verify data types, valid email addresses, positive durations, valid dates, and acceptable appointment times. A field being present does not necessarily mean its value is valid.
3. Calculating Appointment Duration
A booking needs both a start time and an end time. The end time determines the appointment's actual interval and makes it possible to detect overlapping bookings.
In my implementation, the backend calculates the end time using the requested start time and duration:
const startTime = new Date(time);
const endTime = new Date(startTime);
endTime.setMinutes(
endTime.getMinutes() + Number(duration)
);
For example, an appointment starting at 10:00 AM with a duration of 60 minutes should end at 11:00 AM.
Calculating the interval on the backend is important because scheduling decisions should not depend solely on values calculated by the browser.
It is also important to handle time zones consistently. My Google Calendar integration sends ISO-formatted timestamps to the API. A more comprehensive implementation should explicitly define the application's scheduling time zone and validate incoming timestamps to prevent ambiguity around local times and daylight-saving transitions.
4. Preventing Overlapping Bookings
One of the central problems in a booking system is preventing customers from selecting times that have already been reserved.
Two appointments overlap when the first appointment starts before the second one ends and finishes after the second one starts.
The logic can be expressed as:
const overlaps =
requestedStart < existingEnd &&
requestedEnd > existingStart;
This condition also allows back-to-back appointments. For example, a booking ending at 11:00 AM does not conflict with another booking beginning at exactly 11:00 AM.
In my implementation, I retrieve existing bookings from MongoDB, calculate each booking's end time from its start time and duration, and check for conflicts:
const existingBookings = await Booking.find();
const conflict = existingBookings.some(booking => {
const existingStart = new Date(booking.startTime);
const existingEnd = new Date(existingStart);
existingEnd.setMinutes(
existingEnd.getMinutes() +
Number(booking.duration)
);
return (
startTime < existingEnd &&
endTime > existingStart
);
});
if (conflict) {
return res.status(409).json({
message: "This time slot has already been booked"
});
}`
Returning HTTP "409 Conflict" communicates that the request conflicts with an existing booking.
This approach is easy to understand, but it has a scalability limitation: the implementation loads all existing bookings before checking for an overlap. As the collection grows, a database query restricted to potentially overlapping intervals would be more efficient.
There is also an important concurrency consideration. Two requests can check availability at almost the same time, both find the slot free, and both continue. The check alone does not guarantee that simultaneous requests cannot create overlapping bookings.
A stronger production design would use an atomic reservation strategy, such as reserving discrete time slots with a unique database constraint or using another concurrency-control mechanism. That protection needs to be designed around the application's scheduling rules rather than relying only on an initial availability check.
5. Connecting Google Calendar and Google Meet
After the backend confirms that a requested slot does not conflict with an existing booking, it calls a dedicated utility function to create the meeting.
I separated this logic into a Google Calendar utility instead of placing the entire integration directly inside the Express route. This keeps the route focused on the booking workflow and makes the external integration easier to maintain.
Authenticating with Google
The utility initializes an OAuth 2.0 client using environment variables and sets a refresh token:
const { google } = require("googleapis");
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);
oauth2Client.setCredentials({
refresh_token: process.env.GOOGLE_REFRESH_TOKEN
});
The example uses the conventional environment-variable spelling for the client secret. In the project, the environment variable is named "GOOGLE_CLIENT_SECRETE", so the deployed configuration must match the actual code unless that naming is corrected.
The refresh token allows the server-side client to obtain access tokens as needed without requiring the user to repeat the authorization flow for every booking.
This requires the initial OAuth authorization to have been completed successfully and the refresh token to remain valid. The Google account must also have the necessary permissions and configuration for creating Calendar events and conference links.
Credentials should be stored securely in environment variables or an appropriate secrets manager, never committed to a public repository.
Creating a Calendar event
The integration uses Google's Calendar API:
const calendar = google.calendar({
version: "v3",
auth: oauth2Client
});
The meeting function calculates the event's end time and constructs a Calendar event containing the session title, description, start and end timestamps, and attendee information.
The attendee is represented by the customer's email address:
attendees: [
{
email,
responseStatus: "accepted"
}
]
The event also includes settings that prevent guests from modifying the event, inviting other guests, or seeing other guests.
These settings are part of the event configuration; they do not replace proper authorization and access control in the booking application itself.
*Requesting a Google Meet link
*
The event configuration includes a conference creation request:
conferenceData: {
createRequest: {
requestId: "jopad-" + Date.now(),
conferenceSolutionKey: {
type: "hangoutsMeet"
}
}
}
The event is then inserted into the primary Google Calendar:
const response = await calendar.events.insert({
calendarId: "primary",
resource: event,
conferenceDataVersion: 1,
sendUpdates: "all"
});
The "conferenceDataVersion: 1" option tells Google Calendar that conference data should be processed as part of the request. The "sendUpdates: "all"" option requests that Calendar send event updates to the relevant participants.
After the API call succeeds, the implementation extracts the meeting URL from the returned conference data:
const meetLink =
response.data.conferenceData.entryPoints[0].uri;
return meetLink;
The booking route receives that link and includes it in the booking record.
One important implementation detail is that the code assumes the response contains a conference entry point. A more defensive implementation would verify that the response contains a usable Meet URL before continuing.
Google Calendar conference creation can also involve asynchronous processing. Production code should account for cases where the event exists but the conference details are not yet available, rather than assuming every successful event insertion immediately includes a meeting link.
6. Saving Bookings in MongoDB
Once the meeting link has been returned, the backend creates a Mongoose booking document.
The model stores information such as:
- Appointment start time and duration.
- Customer name and contact information.
- Session description.
- Date and time display values.
- Referral information.
- Google Meet link.
- Record creation timestamp.
The central fields include:
const bookingSchema = new mongoose.Schema({
startTime: {
type: Date,
required: true
},
duration: String,
email: String,
firstName: String,
lastName: String,
description: String,
meetLink: String,
createdAt: {
type: Date,
default: Date.now
}
});
This is an excerpt of the schema; the complete project model contains additional fields.
After constructing the document, the route saves it:
await newBooking.save();
Saving the booking after generating the meeting link ensures that the returned booking record can contain the link associated with the created event.
However, the order of these operations creates an important failure scenario. If Google Calendar successfully creates the event but MongoDB subsequently fails to save the booking, a calendar event may remain without a corresponding booking record.
This is a common challenge when coordinating operations across independent services. A more resilient design could track workflow states, retry recoverable operations, and provide a reconciliation or cleanup process for partially completed bookings.
7. Adding an Airtable Backup
The system also attempts to send booking information to Airtable after saving the MongoDB document.
The request includes selected booking fields, such as the customer's name, email, phone number, session description, referral details, meeting link, and appointment date.
The operation is wrapped in its own "try...catch" block. If the Airtable request fails, the error is logged, but the main booking workflow can still continue.
This makes Airtable a secondary destination rather than the primary source of truth for the booking.
That distinction matters: the implementation attempts a backup write, but it does not guarantee that the MongoDB and Airtable records are always synchronized. If Airtable is unavailable, the backup attempt fails without automatically retrying it.
A more robust implementation could place failed backup operations into a retry queue or maintain a record of synchronization status.
8. Sending Booking Notifications
After the booking has been saved and the Airtable attempt has completed, the route triggers the email notification utility:
sendBookingEmails({
email,
firstName,
lastName,
dayString,
referral,
dateString,
number,
description,
timeString,
timeRange,
meetLink
}).catch(err => {
console.log("Email failed:", err.message);
});
The email operation is not awaited by the route. This allows the endpoint to return its successful booking response without waiting for the notification workflow to finish.
It also means an email failure does not change the response after the booking has been saved.
There is a trade-off: the current implementation logs email failures but does not automatically retry them or record a delivery status in the booking document. For a production booking service, a persistent background job or message queue would provide a stronger way to retry notifications and monitor delivery outcomes.
The route eventually responds with HTTP "201 Created", along with a success message and the saved booking.
9. Lessons from the Implementation
Building this workflow brought several backend engineering considerations into focus.
Validate before creating external resources. Checking the request and current bookings before calling Google reduces unnecessary API operations.
Separate responsibilities. Keeping the Calendar integration in its own utility makes the Express route easier to understand and maintain.
Handle partial failures. MongoDB, Google Calendar, Airtable, and an email provider are independent services. A successful operation in one does not guarantee success in the others.
Treat concurrency as a separate problem. Checking for overlapping bookings is necessary, but it does not by itself protect against two simultaneous requests reserving the same slot.
Design for observability and recovery. Logging an error helps with debugging, but production systems also benefit from structured logs, retry mechanisms, delivery tracking, and reconciliation workflows.
These considerations extend beyond booking applications. Any backend that coordinates a database with multiple external APIs needs a plan for partial failures and retries.
Conclusion
Building the JOPAD Advisory booking system from scratch gave me practical experience connecting a Node.js backend to Google Calendar, generating Google Meet links, storing bookings in MongoDB, maintaining a secondary Airtable record, and triggering email notifications.
The most important lesson is that integrating APIs is only one part of building a reliable application. The surrounding workflow—validation, conflict detection, persistence, failure handling, and recovery—is just as important.
A booking endpoint can appear simple from the frontend, but it coordinates several systems that can succeed or fail independently. Understanding those interactions is what turns an API integration into a more complete engineering solution.
I'm continuing to explore ways to make these workflows more resilient, especially around concurrent bookings, retries, and recovery from partial failures.
If you're building a booking platform or integrating third-party services into a Node.js application, I'd be interested to hear how you approach these challenges.
Top comments (0)