A modern, production-ready Next.js 16 frontend for the Reservation System API. Built with the latest React patterns, TypeScript, Tailwind CSS, and shadcn/ui.
- What This Project Teaches
- Prerequisites
- Getting Started
- Environment Variables
- Available Scripts
- Project Architecture
- Learning Concepts
- Application Routes
- Component Library
- Testing
- Development Guidelines
- Troubleshooting
This is a learning repository for modern React frontend development. Every concept is implemented with extensive documentation, inline code comments, and real-world patterns.
- ✅ Server State Management - How to use TanStack Query for data fetching, caching, and synchronization
- ✅ Form Handling - How to build type-safe forms with React Hook Form and Zod validation
- ✅ API Integration - How to create a type-safe API client with error normalization
- ✅ Optimistic Updates - How to make the UI feel instant while waiting for the server
- ✅ Cache Invalidation - How to keep data consistent across the application
- ✅ Error Boundaries - How to gracefully handle and display errors
- ✅ Component Composition - How to build reusable, composable UI components
- ✅ Modern CSS - How to use Tailwind CSS v4 with custom utilities
- ✅ Glass-morphism Design - How to create modern glass-like UI effects
- ✅ Testing with MSW - How to mock API calls for reliable tests
Before running this application, ensure you have:
- Node.js >= 20.0.0
- npm >= 10.0.0
- Backend API running on port 3000 (see main project README)
From the project root (parent directory):
# Install backend dependencies (if not already done)
npm install
# Run database migrations
npm run db:migrate
# Seed the database with sample data
npm run db:seed
# Start the development server
npm run devThe backend will be available at http://localhost:3000.
From this directory (frontend/):
# Install frontend dependencies (if not already done)
npm install
# Start the development server
npm run devThe frontend will be available at http://localhost:3001.
Create a .env.local file in the frontend/ directory:
# Backend API Base URL
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000| Variable | Description | Required |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL |
Base URL of the backend API | Yes |
Note: The NEXT_PUBLIC_ prefix is required for the variable to be accessible in client-side code.
| Script | Description | Port |
|---|---|---|
npm run dev |
Start development server with hot reload | 3001 |
npm run build |
Create production build | - |
npm run start |
Start production server | 3001 |
npm run lint |
Run ESLint | - |
npm test |
Run Vitest tests | - |
npm run test:ui |
Run tests with UI | - |
frontend/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── items/
│ │ │ ├── page.tsx # Items list page
│ │ │ └── [id]/
│ │ │ └── page.tsx # Item detail page
│ │ ├── users/
│ │ │ └── [userId]/
│ │ │ └── reservations/
│ │ │ └── page.tsx # User reservations page
│ │ ├── layout.tsx # Root layout with providers
│ │ ├── page.tsx # Home/dashboard page
│ │ └── globals.css # Global styles
│ ├── components/
│ │ ├── layout/ # Layout components
│ │ │ └── shell.tsx # App shell with navigation
│ │ ├── ui/ # shadcn/ui components
│ │ │ ├── alert.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── sonner.tsx
│ │ │ └── table.tsx
│ │ └── ui-blocks/ # Custom UI blocks
│ │ ├── empty-state.tsx
│ │ ├── error-alert.tsx
│ │ ├── loading-skeleton.tsx
│ │ └── status-badge.tsx
│ ├── lib/
│ │ ├── api/ # API layer
│ │ │ ├── client.ts # HTTP client
│ │ │ ├── endpoints.ts # API endpoint functions
│ │ │ ├── types.ts # TypeScript types
│ │ │ └── index.ts # Public exports
│ │ ├── query/ # TanStack Query setup
│ │ │ ├── keys.ts # Query key definitions
│ │ │ ├── provider.tsx # QueryClient provider
│ │ │ └── index.ts
│ │ └── utils.ts # Utility functions
│ ├── test/ # Test infrastructure
│ │ ├── mocks/ # MSW mocks
│ │ │ ├── data.ts # Mock data factories
│ │ │ ├── handlers.ts # MSW request handlers
│ │ │ ├── server.ts # MSW server setup
│ │ │ └── index.ts # Public exports
│ │ └── setup.ts # Test setup
│ └── ...
├── public/ # Static assets
├── .env.local # Environment variables
├── next.config.ts # Next.js configuration
├── package.json
├── tailwind.config.ts
├── tsconfig.json
└── vitest.config.ts # Vitest configuration
What is Server State? Server state is data that lives on the server and is fetched by the client. Unlike client state (like UI toggles), server state:
- Can be changed by other users
- Needs to be cached for performance
- Can become stale and needs refreshing
- Requires loading and error states
Why TanStack Query? Traditional approaches (useEffect + fetch) require you to manually handle:
- Loading states
- Error handling
- Caching
- Refetching
- Race conditions
TanStack Query handles all of this automatically.
Key Concepts Demonstrated:
// useQuery - For reading data
const { data, isLoading, error } = useQuery({
queryKey: ['item', id],
queryFn: () => getItem(id),
});
// useMutation - For writing data
const mutation = useMutation({
mutationFn: reserveItem,
onSuccess: () => {
// Invalidate related queries to refresh data
queryClient.invalidateQueries({ queryKey: ['items'] });
},
});Query Keys - The Secret to Cache Management:
Query keys are like addresses for your cached data. TanStack Query uses them to:
- Identify cached data
- Determine when to refetch
- Share data between components
// Centralized query keys (src/lib/query/keys.ts)
export const queryKeys = {
items: () => ['items'] as const,
item: (id: string) => ['item', id] as const,
reservations: (userId: string) => ['reservations', userId] as const,
};
// Usage - automatically tied to cache
useQuery({ queryKey: queryKeys.item('item_1'), ... });
// Invalidation - mark as stale to trigger refetch
queryClient.invalidateQueries({ queryKey: queryKeys.items() });Cache Invalidation Strategy:
After mutations, we invalidate queries to keep data fresh:
// After reserving an item:
onSuccess: () => {
// Invalidate items list (stock changed)
queryClient.invalidateQueries({ queryKey: queryKeys.items() });
// Invalidate user's reservations
queryClient.invalidateQueries({
queryKey: queryKeys.reservations(userId)
});
}Why React Hook Form?
- Minimal re-renders (performance)
- Built-in validation support
- Easy error handling
- TypeScript support
Why Zod?
- Runtime type validation
- TypeScript inference
- Declarative schemas
- Great error messages
The Pattern:
// 1. Define schema
const reserveSchema = z.object({
userId: z.string().min(1, 'User ID is required'),
qty: z.coerce.number().int().min(1).max(5),
});
// 2. Infer TypeScript type from schema
type ReserveFormData = z.infer<typeof reserveSchema>;
// 3. Use in component
const form = useForm<ReserveFormData>({
resolver: zodResolver(reserveSchema),
defaultValues: { userId: '', qty: 1 },
});
// 4. Handle submission
const onSubmit = (data: ReserveFormData) => {
mutation.mutate(data);
};Key Features:
- Real-time validation as user types
- Disabled submit while pending
- Clear error messages
- Type-safe data throughout
HTTP Client Abstraction:
We use a centralized HTTP client that handles:
- Request/response formatting
- Error normalization
- Idempotency key generation
- Header management
// src/lib/api/client.ts
export async function apiRequest<T>(
path: string,
options: RequestOptions = {}
): Promise<T> {
// 1. Build URL
// 2. Add headers (including Idempotency-Key if needed)
// 3. Handle {ok: true, data: ...} response format
// 4. Normalize errors to ApiError type
// 5. Return typed data
}Idempotency Keys:
For operations that should only happen once (reserving, confirming), we add an Idempotency-Key header:
// POST with idempotency key
apiPost<Reservation>('/reserve', data, true);
// Adds header: Idempotency-Key: <uuid>
// If network fails and we retry with same key,
// backend returns cached response instead of creating duplicateError Normalization:
All API errors are normalized to a consistent format:
interface ApiError {
status: number; // HTTP status
code: string; // Error code (e.g., "OUT_OF_STOCK")
message: string; // Human-readable
details?: object; // Extra context
requestId?: string; // For debugging
}This allows the UI to handle errors consistently:
- Display user-friendly messages
- Show request ID for support
- Handle specific error codes differently
Three Layers of Error Handling:
-
Global Error Boundary (layout.tsx)
- Catches React render errors
- Shows fallback UI
- Prevents app crashes
-
Query Error Handling (useQuery)
- Catches API errors
- Provides error state to components
- Automatic retry for network errors
-
Mutation Error Handling (useMutation)
- Shows toast notifications
- Keeps UI responsive
- Allows user to retry
Error Display Pattern:
// Component handles both loading and error states
if (isLoading) return <LoadingSkeleton />;
if (error) return <ErrorAlert error={error} />;
return <DataView data={data} />;Defensive Programming:
Always assume the API might return unexpected data:
// Normalize arrays (handle single object responses)
const reservations = Array.isArray(rawReservations)
? rawReservations
: rawReservations ? [rawReservations] : [];
// Safe property access
const count = data?.length ?? 0;Three-Layer Component System:
UI Components (shadcn/ui)
↓
UI Blocks (custom composed components)
↓
Page Components (route-specific)
Layer 1: UI Components (Primitive)
- From shadcn/ui
- Unstyled or minimally styled
- Highly reusable
- Examples: Button, Input, Card
Layer 2: UI Blocks (Composed)
- Domain-specific components
- Combine multiple UI components
- Handle common patterns
- Examples: ErrorAlert, EmptyState
// ErrorAlert combines Alert, Button, and logic
function ErrorAlert({ error, onRetry }) {
return (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error.message}</AlertDescription>
{error.requestId && <CopyButton text={error.requestId} />}
{onRetry && <Button onClick={onRetry}>Retry</Button>}
</Alert>
);
}Layer 3: Page Components (Route-Specific)
- Use UI Blocks and UI Components
- Handle data fetching
- Route-specific logic
- Examples: ItemsPage, ItemDetailPage
Tailwind CSS v4 Features Used:
- @theme Directive - Define custom CSS variables
@theme {
--color-primary: hsl(var(--primary));
--radius-lg: var(--radius);
}- @utility Directive - Create reusable utility classes
@utility {
.glass {
backdrop-blur: 24px;
background-color: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.2);
}
}- CSS Custom Properties - Dynamic theming
:root {
--primary: 262.1 83.3% 57.8%;
--background: 0 0% 100%;
}Glass-morphism Design:
The app uses a modern glass-morphism aesthetic:
- Semi-transparent backgrounds
- Backdrop blur effects
- Subtle borders and shadows
- Gradient accents
.glass {
backdrop-blur-xl bg-white/70
border border-white/20
shadow-xl rounded-xl
}- Displays API health status
- Shows database and cache health
- Quick navigation links to Items and Reservations
- Grid view of all available items
- Stock level indicators
- Links to item detail pages
- Detailed item information
- Reservation Form with:
- User ID input
- Quantity selector (1-5)
- Client-side validation
- Idempotency key handling
- Real-time stock updates
- List of all reservations for a user
- Active reservations with confirm/cancel actions
- Past reservations (completed/cancelled/expired)
- Status badges and expiration timers
| Component | Usage |
|---|---|
Button |
Actions, form submission |
Card |
Content containers |
Input |
Form fields |
Label |
Form labels |
Badge |
Status indicators |
Alert |
Error messages |
Table |
Reservations list |
Separator |
Visual dividers |
Sonner |
Toast notifications |
| Component | Purpose |
|---|---|
Shell |
App layout with navigation |
ErrorAlert |
API error display with request ID |
EmptyState |
Friendly empty state messages |
LoadingSkeleton |
Skeleton loaders for lists |
StatusBadge |
Reservation status badges |
Test Stack:
- Vitest - Test runner (Vite-native)
- React Testing Library - Component testing
- MSW - API mocking
- jsdom - Browser environment
Testing Philosophy:
-
Test Behavior, Not Implementation
// Good - Tests what user sees expect(screen.getByText('Reserve Now')).toBeInTheDocument(); // Avoid - Tests implementation details expect(component.state.isOpen).toBe(true);
-
Mock at Network Level
// MSW intercepts actual HTTP requests const server = setupServer(handlers); // Your code makes real requests, MSW returns mocks
-
Fresh Data Per Test
beforeEach(() => { resetMockData(); // Prevent test pollution });
Running Tests:
# Run all tests
npm test
# Run with UI
npm run test:ui
# Run in watch mode
npm run test:watch- Create the route directory in
src/app/ - Add
page.tsxwith 'use client' directive for interactive pages - Use TanStack Query for data fetching
- Add loading and error states
- Update navigation in
Shellcomponent if needed
- Add type definitions in
src/lib/api/types.ts - Add endpoint function in
src/lib/api/endpoints.ts - Export from
src/lib/api/index.ts - Use in components with
useQueryoruseMutation
- Define Zod schema for validation
- Use
react-hook-formwithzodResolver - Show loading state during submission
- Display success toast on completion
- Show error with request ID on failure
Problem: "Unable to connect to the server" error
Solution:
- Ensure backend is running on port 3000
- Check
NEXT_PUBLIC_API_BASE_URLin.env.local - Verify CORS is configured correctly in backend
Problem: CORS errors in browser console
Solution:
Update backend .env:
CORS_ORIGIN=http://localhost:3001Or keep as * for development.
Problem: "Port 3001 is already in use"
Solution:
# Find and kill process on port 3001
lsof -ti:3001 | xargs kill -9Problem: Module not found errors
Solution:
# Clear Next.js cache
rm -rf .next
# Reinstall dependencies
rm -rf node_modules package-lock.json
npm installHere's a complete walkthrough of the application:
-
Start both servers:
# Terminal 1 - Backend cd .. && npm run dev # Terminal 2 - Frontend npm run dev
-
Open the app: Navigate to
http://localhost:3001 -
Check API status: The home page shows if the backend is connected
-
Browse items: Click "Browse Items" or go to
/items -
Make a reservation:
- Click on any item
- Enter a user ID (default:
demo-user) - Select quantity (1-5)
- Click "Reserve Now"
-
Manage reservations:
- Go to
/users/demo-user/reservations - See your active reservation
- Confirm or cancel as needed
- Go to
-
Verify idempotency:
- Try reserving the same item twice with the same Idempotency-Key
- The second request returns the cached response
| Technology | Purpose | Version |
|---|---|---|
| Next.js | React framework | 16.1.6 |
| React | UI library | 19.2.3 |
| TypeScript | Type safety | 5.x |
| Tailwind CSS | Styling | 4.x |
| shadcn/ui | Component library | Latest |
| TanStack Query | Server state | Latest |
| React Hook Form | Form handling | Latest |
| Zod | Validation | Latest |
| Lucide React | Icons | Latest |
| Vitest | Testing | Latest |
| MSW | API mocking | Latest |
MIT - Same as the main project.
For issues or questions:
- Check the Troubleshooting section
- Review the main project README
- Check browser console for error details
- Note the Request ID from error messages for debugging