How to add booking to a Next.js app with a REST API

Stanislav TyshchenkoTutorial10 min readJul 21, 2026
Adding appointment booking to a Next.js app with a REST API

Adding a booking page to a Next.js app looks like a weekend project until you start it. The UI is the easy part. The hard part is everything the calendar needs to know before it can show a single open slot: which staff are working, how long the service takes, what's already booked, how much buffer to leave between appointments, and whether two people can hold the same slot at once. That logic is where booking projects quietly turn into month-long ones.

This guide takes the other path. Instead of building the scheduling engine yourself, you call a booking API for the availability, cart, and payment logic, and you keep full control of the Next.js frontend. I built Opencals as exactly that kind of API, so the code here uses it — but the shape of the problem is the same whichever backend you pick, and I'll flag the parts that are universal.

The frontend of a booking flow is a few forms and a calendar grid. The backend is real-time availability across staff, services, and existing bookings — plus payments. Calling a booking API lets you own the first part and hand off the second.

What "add booking" actually involves

Before any code, it helps to name the pieces. A working booking flow has four moving parts, and most half-finished projects stall because they underestimated one of them.

1

A service catalog

What can be booked — a 60-minute session, a color treatment, a group class — each with its own duration, price, and the staff who can deliver it. This is your booking homepage data.

2

Real-time availability

Given a service and a date range, which slots are actually open? This has to account for existing bookings, staff hours, buffer time, and multi-staff conflicts. It is the single hardest piece to build well.

3

A cart and checkout

Holding a slot while the customer enters details and pays, then confirming the booking only if payment succeeds. Slots are a scarce resource, so this needs to be race-safe.

4

Post-booking management

Confirmation, reminders, and letting customers reschedule or cancel without emailing you. Easy to skip on day one, painful to add later.

If you build all four yourself, you're writing a scheduling engine. If you call an API, you're wiring four typed methods into React components. This guide does the second.

Prerequisites

You need a Next.js 14+ app using the App Router, Node 18+, and a booking API key. With Opencals you get a storefront key (prefixed sfk_) from the dashboard under Settings → API; any booking API will have an equivalent. Install the SDK:

bash
npm i @opencals/storefront-sdk

Then set the key in your environment. Keep it server-side — a storefront key can create real bookings, so it does not belong in client bundles.

bash
# .env.local OPENCALS_API_KEY=sfk_live_your_key_here

Why server-side

Anything that creates a booking or a payment should run in a Route Handler or Server Action, never in a client component. The browser calls your Next.js API routes; your routes call the booking API with the secret key. This is the same pattern you'd use for a Stripe secret key.

Step 1 — Initialise the SDK once

Create a small module that configures the client. Everything server-side imports from here, so the key lives in exactly one place.

typescript
// lib/opencals.ts import { setupOpencals } from "@opencals/storefront-sdk"; export const opencals = setupOpencals({ apiKey: process.env.OPENCALS_API_KEY!, });

If you prefer raw fetch over an SDK, the same call is a GET with a bearer token — the SDK just gives you typed responses. Both work; typing saves you from guessing the shape of a nested availability object at 11pm.

bash
curl https://api.opencals.com/storefront/products \ -H "Authorization: Bearer sfk_live_your_key"

Step 2 — List services on a Server Component

The App Router lets you fetch on the server and stream HTML, which is ideal for a service catalog — it's public data, good for SEO, and doesn't need to be interactive.

typescript
// app/book/page.tsx import { opencals } from "@/lib/opencals"; import { ProductService } from "@opencals/storefront-sdk"; import { ServiceCard } from "./service-card"; export default async function BookPage() { const { data: products } = await ProductService.listProducts({ opencals }); return ( <main className="grid gap-4"> {products.map((service) => ( <ServiceCard key={service.id} service={service} /> ))} </main> ); }

products is fully typed — service.variants carries per-variant duration and price, service.staff the practitioners who can deliver it. You render, you don't compute. The booking API already resolved which staff map to which service.

Step 3 — Fetch real-time availability

This is the endpoint that would have taken you weeks to write. You pass a service, a date range, and optionally a staff member; you get back the slots that are genuinely open right now.

typescript
// app/api/availability/route.ts (Route Handler) import { NextRequest, NextResponse } from "next/server"; import { opencals } from "@/lib/opencals"; import { AppointmentService } from "@opencals/storefront-sdk"; export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const productId = searchParams.get("productId")!; const from = searchParams.get("from")!; const to = searchParams.get("to")!; const { data: slots } = await AppointmentService.getAvailability({ opencals, productId, from, to, }); return NextResponse.json({ slots }); }

The returned slots already exclude times that are booked, outside staff hours, or too close to another appointment given the service's buffer. Your calendar component fetches this route when the user picks a date and renders whatever comes back — no client-side conflict math.

The universal part

Every booking backend worth using exposes an availability endpoint like this. If a tool only gives you raw calendar events and expects you to compute open slots yourself, you haven't offloaded the hard part — you've just moved it. Availability calculation is the feature to evaluate first.

Step 4 — Hold the slot with a cart

A slot is a scarce resource. Between "customer clicked 2pm" and "customer paid," you don't want someone else grabbing it, and you don't want to create a real booking that might never be paid for. A cart holds the slot for a short window.

typescript
// app/api/cart/route.ts import { NextRequest, NextResponse } from "next/server"; import { opencals } from "@/lib/opencals"; import { CartService } from "@opencals/storefront-sdk"; export async function POST(req: NextRequest) { const { productId, variantId, slot } = await req.json(); const { data: cart } = await CartService.addItem({ opencals, productId, variantId, startTime: slot, }); return NextResponse.json({ cartId: cart.id, cart }); }

Store cartId in the user's session (a cookie or your session store). It's the handle you'll pass to checkout.

Step 5 — Checkout and confirm

Checkout turns a held cart into a paid booking. The important property here is idempotency: payment flows fail and retry in the real world, and a repeated request must not create two bookings or charge twice.

typescript
// app/api/checkout/route.ts import { NextRequest, NextResponse } from "next/server"; import { opencals } from "@/lib/opencals"; import { CheckoutService } from "@opencals/storefront-sdk"; export async function POST(req: NextRequest) { const { cartId, customer } = await req.json(); // 1. Create a payment intent for the cart total const { data: intent } = await CheckoutService.initiate({ opencals, cartId, customer, }); // 2. Client confirms the payment with intent.clientSecret, // then you confirm the booking: const { data: booking } = await CheckoutService.confirm({ opencals, cartId, paymentIntentId: intent.id, }); return NextResponse.json({ booking }); }

The two-step split — initiate returns a payment intent, the browser confirms the card, then confirm finalises the booking — is what keeps a failed card from leaving a phantom appointment on the calendar. The slot only becomes a real booking once money has actually moved.

Step 6 — Let customers manage their own bookings

The reschedule-and-cancel flow is the one teams skip first and regret most, because without it every change becomes a support message. These endpoints work off a customer token issued by your own auth flow, not the store key — the customer can only see and touch their own bookings.

typescript
const { data: bookings } = await AppointmentService.listCustomerBookings({ opencals, customerToken, // issued by your auth, scoped to one customer }); await AppointmentService.reschedule({ opencals, customerToken, bookingId, newStartTime, });

Wire these into an account page and the reschedule requests that used to hit your inbox now resolve themselves.

Putting it together

The full flow, from the frontend's point of view, is five typed calls:

1

List services

ProductService.listProducts() → render the catalog on a Server Component.

2

Check availability

AppointmentService.getAvailability() → the open slots for a date, computed server-side.

3

Add to cart

CartService.addItem() → hold the chosen slot while the customer checks out.

4

Initiate checkout

CheckoutService.initiate() → a payment intent the browser confirms.

5

Confirm booking

CheckoutService.confirm() → the slot becomes a real, paid booking.

Everything hard about scheduling — the availability math, the race conditions, the payment-to-booking handoff — lives behind those calls. Your Next.js app stays a frontend: forms, a calendar grid, and route handlers that proxy to the API with your secret key.

When not to use an API for this

Honest caveat, because not every project needs this. If you're a solo consultant booking 1:1 calls and you don't take payment up front, a hosted tool like Calendly is simpler and you shouldn't build anything. An API earns its keep when you need a custom-branded flow, multiple staff or locations, payments at booking, or booking embedded inside a product you already own. That's when owning the frontend while renting the backend is the right trade.

If that's you, owning the frontend while renting the backend is the right trade — and the links below cover the endpoint surface, the architecture, and the typed SDK.

Where to go from here

FAQ

Early Access — 3 Months Free

Ready to transform your service business?

Join 150+ businesses already using Opencals. Get 3 months completely free with all features unlocked.

No credit card required
Setup in 10 minutes
Cancel anytime