How to Build a Booking App with AI Using React Native
A step-by-step tutorial for building a complete booking and appointment app with an AI agent - exact prompts, generated architecture, iteration strategy, and the specific patterns that make booking flows work in React Native.
What We're Building
A booking app is one of the best tests of an AI agent's architecture decisions. Unlike a simple CRUD app, a booking system requires time-slot management, conflict detection, multi-step forms, confirmation flows, and calendar UI - all things that need to work together without contradictions.
By the end of this tutorial, you'll have a working React Native booking app running on your phone. I'll show you the exact prompts, the file structure the agent generated, the code patterns worth understanding, and the iterations that turned a good first draft into something you could actually hand to a client.
If you're new to AI agent app builders and want to understand what's happening under the hood, read What Is an AI Agent App Builder? first. This post focuses on the practical build.
Why a Booking App?
Booking apps are the most requested category I see from founders and agencies. Hair salons, fitness studios, consulting firms, medical practices, tutoring services - any business that sells time needs some version of this. And the core patterns are consistent across all of them:
- A service catalog (what can be booked)
- A provider/staff list (who provides the service)
- A time-slot picker with availability logic
- A booking confirmation flow
- A view of upcoming and past appointments
The differences between a salon booking app and a consulting booking app are mostly cosmetic. The architecture is the same. Build one well and you understand the pattern for all of them.
The Prompt
Here's the exact prompt I used. I'll break down why each part matters afterward:
Build a React Native booking app using Expo and TypeScript called "BookIt". The app is for a boutique hair salon. It should have four tabs: Home (today's appointments + quick book button), Services (list of services with duration and price), Book (multi-step booking flow: select service → select stylist → pick date/time → confirm), and Profile (user info, booking history, cancel/reschedule options). Use a clean, modern design with a warm white background (#fafaf9), dark charcoal text (#1c1917), and rose gold accent (#e11d48 for primary buttons, #fda4af for secondary elements). Include 6 realistic salon services with names, durations (30-90 min), prices ($35-$120), and descriptions. Include 4 stylists with names, specialties, and availability schedules. Time slots should be 30-minute intervals from 9am to 6pm. Use AsyncStorage for local persistence. The booking flow should show only available time slots (not already booked).
Let me explain the key decisions:
Named the app and the business type. "BookIt" and "boutique hair salon" give the agent specific context. A salon booking app has different UX expectations than a medical appointment app - shorter service durations, multiple providers with different specialties, walk-in friendly design. Specified the multi-step booking flow explicitly. This is the most architecturally complex part of a booking app. Without spelling out the steps (service → stylist → date/time → confirm), the agent might generate a single-screen form that crams everything together, or skip the stylist selection entirely. Defined the data shape. 6 services with durations and prices, 4 stylists with specialties and availability. This prevents generic placeholder data ("Service 1", "Provider A") and forces the agent to generate realistic mock data that makes the demo actually feel like a real product. Specified the availability logic. "Show only available time slots (not already booked)" is a single sentence that triggers the agent to build conflict detection - checking existing bookings against the schedule before rendering available slots. Without this instruction, you'd get a time picker that shows all slots regardless of whether they're taken. Color values with purpose. Rose gold (#e11d48) as primary with soft pink (#fda4af) as secondary. These aren't random - they match the beauty/salon industry. The warm white (#fafaf9) background instead of pure white gives the whole app a softer, more premium feel.What the Agent Generated
The agent produced 26 files in about two minutes. Here's the complete structure:
``
bookit/
├── package.json # Expo SDK 51, dependencies declared
├── app.json # Expo config with app name and icons
├── tsconfig.json # Strict mode, path aliases
├── babel.config.js # Expo preset
├── app/
│ ├── _layout.tsx # Root layout with tab navigator
│ ├── (tabs)/
│ │ ├── _layout.tsx # Tab bar configuration
│ │ ├── index.tsx # Home tab - today's appointments
│ │ ├── services.tsx # Service catalog
│ │ ├── book.tsx # Multi-step booking flow
│ │ └── profile.tsx # User profile + history
├── components/
│ ├── ServiceCard.tsx # Service list item with price/duration
│ ├── StylistCard.tsx # Stylist selection card
│ ├── TimeSlotGrid.tsx # Available time slot picker
│ ├── BookingConfirmation.tsx # Confirmation screen with summary
│ ├── AppointmentCard.tsx # Upcoming/past appointment display
│ ├── StepIndicator.tsx # Progress dots for booking flow
│ └── EmptyState.tsx # Shared empty state component
├── hooks/
│ ├── useBookings.ts # Booking CRUD + AsyncStorage
│ ├── useAvailability.ts # Time slot availability calculation
│ └── useServices.ts # Service data access
├── types/
│ └── booking.ts # TypeScript interfaces
├── constants/
│ ├── theme.ts # Colors, spacing, typography
│ ├── services.ts # 6 salon services with full details
│ └── stylists.ts # 4 stylists with schedules
└── utils/
├── dateUtils.ts # Date formatting, slot generation
└── bookingUtils.ts # Conflict detection, validation
`
26 files with clear separation of concerns. The architecture decisions worth noting:
useAvailability.ts as a separate hook. The agent correctly identified that availability calculation is complex enough to warrant its own hook, separate from the booking CRUD operations. This is the kind of architectural decision that separates a maintainable codebase from a tangled one.
StepIndicator.tsx component. The multi-step booking flow needs visual progress indication. The agent created a reusable step indicator rather than hardcoding progress dots into the booking screen - which means if you later add a fifth step (like payment), you change one prop instead of rewriting the UI.
bookingUtils.ts for conflict detection. The availability logic lives in a pure utility function, not inside a component or hook. This makes it testable, reusable, and easy for a developer to understand when they inherit the codebase.
The Code That Matters
The Type System
`typescript
// types/booking.ts
export interface Service {
id: string;
name: string;
description: string;
duration: number; // minutes
price: number;
category: ServiceCategory;
}
export type ServiceCategory = "cut" | "color" | "treatment" | "styling";
export interface Stylist {
id: string;
name: string;
specialty: string;
avatar: string; // initials for avatar circle
availability: WeeklySchedule;
}
export interface WeeklySchedule {
[day: string]: { start: string; end: string } | null;
// null means day off
}
export interface Booking {
id: string;
serviceId: string;
stylistId: string;
date: string; // YYYY-MM-DD
startTime: string; // HH:mm
endTime: string; // HH:mm
status: BookingStatus;
createdAt: string;
}
export type BookingStatus = "confirmed" | "cancelled" | "completed";
export interface TimeSlot {
time: string; // HH:mm
available: boolean;
}
`
The WeeklySchedule type using null for days off is a clean pattern. It avoids boolean flags (isAvailable: true) and instead uses the absence of data to indicate unavailability. The TimeSlot interface separating the time string from the availability boolean keeps the rendering logic clean - the grid component gets a flat array of slots and just checks the available flag.
The Availability Hook
This is the most important piece of logic in the entire app:
`typescript
// hooks/useAvailability.ts
import { useMemo } from "react";
import { Booking, Stylist, Service, TimeSlot } from "@/types/booking";
import { generateTimeSlots, isSlotConflicting } from "@/utils/bookingUtils";
export function useAvailability(
stylist: Stylist | null,
service: Service | null,
date: string,
existingBookings: Booking[]
) {
const availableSlots = useMemo(() => {
if (!stylist || !service || !date) return [];
const dayOfWeek = new Date(date).toLocaleDateString("en-US", {
weekday: "lowercase",
});
const schedule = stylist.availability[dayOfWeek];
// Stylist doesn't work this day
if (!schedule) return [];
// Generate all possible slots for the day
const allSlots = generateTimeSlots(
schedule.start,
schedule.end,
30 // 30-minute intervals
);
// Filter out slots that conflict with existing bookings
return allSlots.map((slot): TimeSlot => ({
time: slot,
available: !isSlotConflicting(
slot,
service.duration,
date,
stylist.id,
existingBookings
),
}));
}, [stylist, service, date, existingBookings]);
return { availableSlots };
}
`
The useMemo dependency array is correct - it recalculates when the stylist, service, date, or existing bookings change. The early returns for missing data prevent unnecessary computation. And the conflict check happens per-slot, per-stylist, which is exactly right for a salon where different stylists can have overlapping appointments but the same stylist cannot.
The Conflict Detection Utility
`typescript
// utils/bookingUtils.ts
export function isSlotConflicting(
slotTime: string,
serviceDuration: number,
date: string,
stylistId: string,
bookings: Booking[]
): boolean {
const slotStart = timeToMinutes(slotTime);
const slotEnd = slotStart + serviceDuration;
return bookings.some((booking) => {
if (booking.date !== date) return false;
if (booking.stylistId !== stylistId) return false;
if (booking.status === "cancelled") return false;
const bookingStart = timeToMinutes(booking.startTime);
const bookingEnd = timeToMinutes(booking.endTime);
// Check for any overlap
return slotStart < bookingEnd && slotEnd > bookingStart;
});
}
function timeToMinutes(time: string): number {
const [hours, minutes] = time.split(":").map(Number);
return hours * 60 + minutes;
}
`
This is textbook interval overlap detection: two intervals overlap if one starts before the other ends AND ends after the other starts. The function correctly filters by date, stylist, and excludes cancelled bookings. A developer reviewing this code would recognize the pattern immediately - no cleanup needed.
The Multi-Step Booking Flow
The booking screen uses a step-based state machine:
`typescript
// app/(tabs)/book.tsx (simplified)
type BookingStep = "service" | "stylist" | "datetime" | "confirm";
export default function BookScreen() {
const [step, setStep] = useState
const [selectedService, setSelectedService] = useState
const [selectedStylist, setSelectedStylist] = useState
const [selectedDate, setSelectedDate] = useState
const [selectedTime, setSelectedTime] = useState
const { bookings, createBooking } = useBookings();
const { availableSlots } = useAvailability(
selectedStylist,
selectedService,
selectedDate,
bookings
);
function handleNext() {
const steps: BookingStep[] = ["service", "stylist", "datetime", "confirm"];
const currentIndex = steps.indexOf(step);
if (currentIndex < steps.length - 1) {
setStep(steps[currentIndex + 1]);
}
}
function handleBack() {
const steps: BookingStep[] = ["service", "stylist", "datetime", "confirm"];
const currentIndex = steps.indexOf(step);
if (currentIndex > 0) {
setStep(steps[currentIndex - 1]);
}
}
// Render current step...
}
`
The step array pattern is cleaner than a switch statement with hardcoded next/previous logic. Adding a step later (say, "extras" for add-on services) means adding one string to the array and one render case.
The Time Slot Grid
This component deserves attention because it's the core UI interaction:
`typescript
// components/TimeSlotGrid.tsx
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { TimeSlot } from "@/types/booking";
import { theme } from "@/constants/theme";
interface TimeSlotGridProps {
slots: TimeSlot[];
selectedTime: string;
onSelect: (time: string) => void;
}
export function TimeSlotGrid({ slots, selectedTime, onSelect }: TimeSlotGridProps) {
if (slots.length === 0) {
return (
);
}
return (
{slots.map((slot) => (
key={slot.time} style={[ styles.slot, !slot.available && styles.slotUnavailable, selectedTime === slot.time && styles.slotSelected, ]} onPress={() => slot.available && onSelect(slot.time)} disabled={!slot.available} activeOpacity={slot.available ? 0.7 : 1} > style={[ styles.slotText, !slot.available && styles.slotTextUnavailable, selectedTime === slot.time && styles.slotTextSelected, ]} > {formatTime(slot.time)}
))}
);
}
`
The conditional styling pattern ([styles.slot, !slot.available && styles.slotUnavailable]) is idiomatic React Native. Unavailable slots are visually distinct but still rendered - removing them entirely would create confusing gaps in the grid and make it harder for users to understand which times are taken.
Running the App
Download the zip, extract, and run:
`bash
cd bookit
npm install
npx expo start
`
Scan the QR code with Expo Go on your phone. The app loads in about 10 seconds over your local network. No Xcode, no Android Studio needed for the initial preview.
If you want simulators:
`bash
# iOS (Mac only, requires Xcode)
npx expo start --ios
# Android (requires Android Studio)
npx expo start --android
`
Iterations That Made the Difference
The first generation was solid but needed refinement. Here's the exact sequence:
Iteration 1 - Date picker UX: "The date picker on the booking screen is a basic text input. Replace it with a horizontal scrolling date strip showing the next 14 days. Each day shows the day name (Mon, Tue) and date number. Today should be pre-selected. Days when the selected stylist is off should be grayed out with a small 'Off' label."This changed the booking flow from feeling like a form to feeling like a native app. Horizontal date strips are the standard pattern in booking apps (Calendly, Acuity, ClassPass) because they reduce friction - users don't need to open a modal calendar.
Iteration 2 - Confirmation design: "The booking confirmation screen needs more visual weight. Add a large checkmark animation at the top (use a simple scale-in animation with Animated API), show the full booking summary in a card (service name, stylist, date, time, duration, price), and add two buttons: 'Add to Calendar' (placeholder) and 'Book Another'." Iteration 3 - Home screen polish: "On the Home tab, separate today's appointments into 'Upcoming' (future time today) and 'Completed' (past time today) sections. Add a greeting at the top that says 'Good morning/afternoon/evening' based on the current time. The quick book button should be a prominent floating action button in the bottom right, not inline." Iteration 4 - Service cards: "The service cards on the Services tab need the duration displayed more prominently. Show it as a pill badge next to the price (e.g., '60 min' in a soft rose badge). Also add a thin left border to each card in the accent color to give the list more visual rhythm."Each iteration took 30-50 seconds. Four rounds and the app went from "functional prototype" to "something that looks designed."
Adapting This for Other Business Types
The booking app pattern translates directly. Here's what changes and what stays the same:
For a fitness studio: Services become classes. Stylists become instructors. Add a "spots remaining" count to each time slot. The availability logic is identical - you're still checking for conflicts, just with capacity > 1 instead of capacity = 1. For a consulting firm: Services become meeting types (30-min intro, 60-min strategy session). There's usually one provider, not multiple. The time slot grid can be wider (60-minute intervals). Add a "notes" field to the booking form for meeting agenda. For a medical practice: Add a "reason for visit" dropdown to the booking flow. Time slots might be 15-minute intervals. The confirmation screen needs a "pre-visit instructions" section. The profile tab needs insurance information fields. For a tutoring service: Services become subjects. Add a "virtual/in-person" toggle to the booking flow. The stylist card becomes a tutor card with ratings and subjects taught.In each case, you're modifying the prompt, not the architecture. The same four-tab structure, the same multi-step booking flow, the same availability logic.
Common Mistakes with Booking App Prompts
Mistake 1: Not specifying the time slot interval. If you don't say "30-minute intervals," the agent might generate hourly slots (too coarse for most services) or 15-minute slots (too granular, creates an overwhelming grid). Mistake 2: Forgetting the cancellation flow. Add "users should be able to cancel bookings from their profile, with a confirmation dialog" to your prompt. Otherwise you'll need an extra iteration to add it. Mistake 3: Not defining the availability model. "Stylists have availability" is too vague. "Stylists have per-day availability with start and end times, and some days off" gives the agent the data structure it needs. Mistake 4: Cramming payment into the MVP. Real payment processing requires a backend (Stripe, Square). For validation, a booking confirmation with a price displayed is sufficient. Users understand it's a prototype. Don't let payment integration block your ability to test the core booking flow.What You Own
The generated code is yours. No watermarks, no SDK dependency, no "powered by" requirements. It's standard React Native with Expo - any React Native developer can read, modify, and extend it. The booking logic in useAvailability.ts and bookingUtils.ts` is production-quality architecture that a developer can build on directly.
If you want to take this to production, the next steps are:
For context on how AI agent generation compares to building on no-code platforms like Bubble or FlutterFlow, read AI Agent App Builder vs No-Code Platforms. For another tutorial with a different app type, the habit tracker walkthrough covers the full generation-to-device workflow.
Ready to build your app?
Start Building for Free