Contents

Chapter 1

Architecture Guide

This document explains how the React SaaS Template is structured, how data flows through the application, and the reasoning behind each architectural decision.

High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Browser                       β”‚
β”‚                                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚  AuthProviderβ”‚   β”‚  AppRoutes  β”‚               β”‚
β”‚  β”‚  (Context)   │──▢│  (Router)   β”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚         β”‚                  β”‚                      β”‚
β”‚         β–Ό                  β–Ό                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚  useAuth()  β”‚   β”‚  Pages      β”‚               β”‚
β”‚  β”‚  hook       β”‚   β”‚  (lazy)     β”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚         β”‚                  β”‚                      β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β”‚                  β–Ό                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
β”‚  β”‚          API Client               β”‚            β”‚
β”‚  β”‚  - Token injection                β”‚            β”‚
β”‚  β”‚  - 401 refresh flow               β”‚            β”‚
β”‚  β”‚  - Retry logic                    β”‚            β”‚
β”‚  β”‚  - Error normalization            β”‚            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                 β”‚                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ HTTPS
                  β–Ό
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  Your Backend  β”‚
         β”‚  REST API      β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Provider Architecture

The app wraps everything in an AuthProvider at the top level. This context holds the current user, the access token, and login/logout/signup methods. Any component can call useAuth() to read or modify auth state.

Why a context instead of a state manager like Redux or Zustand?

Auth state is "global" but rarely changes β€” it's set once at login and cleared at logout. A React context is the simplest correct tool for this. Adding Redux for a single piece of global state would be over-engineering. If your app grows to need client-side caching, global UI state, or complex derived state, consider adding Zustand or TanStack Query at that point.

Routing Strategy

Routes are defined in src/routes.tsx using react-router-dom v6. Key decisions:

Code Splitting

Every page component is wrapped in React.lazy(), so Vite creates a separate JavaScript chunk for each page. Users only download the code for the page they're visiting. The boundary shows a spinner while the chunk loads.

Protected Routes

The component sits in the route tree as a layout route. It checks useAuth().isAuthenticated and either renders (the child routes) or redirects to /login. This means you never need to add auth checks inside individual page components.

Route Organization

/login              β†’ Public
/signup             β†’ Public
/forgot-password    β†’ Public
/pricing            β†’ Public
/dashboard          β†’ Protected β†’ DashboardLayout
/settings           β†’ Protected β†’ DashboardLayout
/billing            β†’ Protected β†’ DashboardLayout

All protected routes share the DashboardLayout (sidebar + top bar). Adding a new authenticated page is just adding a inside the protected block.

API Client Design

src/lib/api-client.ts is the single gateway for all network requests. Here's why each feature exists:

Token Injection

Every request automatically includes Authorization: Bearer unless you pass skipAuth: true (used for login/signup where there's no token yet).

Automatic Token Refresh

When the server returns 401, the client calls /auth/refresh before retrying. The refresh token lives in an httpOnly cookie β€” not in JavaScript β€” to limit XSS exposure. Multiple concurrent 401s are deduplicated so you don't send 5 refresh requests simultaneously.

Retry with Backoff

Server errors (500, 502, 503, 504) and rate limits (429) are retried up to 2 times with increasing delays. Client errors (400, 403, 404, 422) are never retried because they indicate a bug or invalid input, not a transient failure.

Error Normalization

Every error is parsed into an ApiError object with status, message, and optional details (field-level validation errors). This means your catch blocks can always assume the same shape regardless of whether the error came from a network failure, a server crash, or a validation rejection.

State Management Patterns

This template uses local component state (useState) for everything. Here's the decision framework for when to "upgrade":

State TypeWhere to Put ItExample
Form valuesuseState in the form componentLogin form fields
UI state (open/closed)useState in the owning componentSidebar toggle, modal visibility
Server datauseState + useEffect fetchDashboard stats, invoice list
Auth stateReact ContextCurrent user, access token
Cross-page stateURL search params or React ContextFilters, pagination

When to add a data-fetching library:

If you find yourself writing the same useState + useEffect + isLoading + error pattern in more than 5 components, adopt TanStack Query (React Query). It handles caching, deduplication, background refetching, and optimistic updates.

Component Organization

Components are organized by domain (auth, billing, dashboard) rather than by type (buttons, forms, layouts). This means all the code for a feature lives in one folder.

The ui/ folder is the exception β€” it contains generic, reusable primitives that aren't tied to any business domain. These are the building blocks that domain components compose.

components/
β”œβ”€β”€ auth/           ← Auth feature
β”‚   β”œβ”€β”€ LoginForm
β”‚   β”œβ”€β”€ SignupForm
β”‚   β”œβ”€β”€ ForgotPasswordForm
β”‚   └── ProtectedRoute
β”œβ”€β”€ billing/        ← Billing feature
β”‚   β”œβ”€β”€ PricingTable
β”‚   └── SubscriptionManager
β”œβ”€β”€ dashboard/      ← Dashboard feature
β”‚   β”œβ”€β”€ DashboardLayout
β”‚   β”œβ”€β”€ Sidebar
β”‚   └── DashboardPage (with StatsCard, DataTable, ActivityFeed)
β”œβ”€β”€ settings/       ← Settings feature
β”‚   └── SettingsPanel
└── ui/             ← Shared primitives
    β”œβ”€β”€ Button
    β”œβ”€β”€ Input
    β”œβ”€β”€ Modal
    β”œβ”€β”€ Badge
    └── ...

Stripe Integration Pattern

The billing flow uses Stripe Checkout (redirect-based), not Stripe Elements (embedded). This is intentional:

1. PCI compliance: Stripe Checkout is the lowest-PCI-burden integration. Card numbers never touch your frontend or backend.

2. Maintenance: Stripe maintains the checkout UI, handles 3D Secure, Apple Pay, etc. You don't need to build or maintain payment forms.

3. Conversion: Stripe continuously A/B tests their checkout page for conversion optimization.

The flow:

1. User clicks "Get started" on a plan β†’ calls your backend POST /billing/checkout-session

2. Backend creates a Stripe Checkout Session and returns the session ID

3. Frontend redirects to Stripe's hosted checkout page

4. After payment, Stripe redirects back to your successUrl

5. Stripe sends a webhook to your backend confirming the payment

6. Backend activates the subscription and updates the database

For subscription management (upgrade, cancel, update payment method), the template uses Stripe's Customer Portal β€” another hosted page that Stripe maintains.

Environment Configuration

src/config/env.ts validates environment variables at import time. This is a deliberate trade-off:

  • Pro: Missing variables fail immediately with a clear error instead of producing undefined deep in a fetch call
  • Con: The app won't render at all if a required variable is missing

This is the right trade-off for a SaaS app. A blank screen with "Missing VITE_API_BASE_URL" in the console is far easier to debug than a screen that loads but silently sends requests to undefined/api/auth/me.

Chapter 2
πŸ”’ Available in full product

Deployment Guide

Chapter 3
πŸ”’ Available in full product

Adding a Feature: Step-by-Step Walkthrough

Chapter 4
πŸ”’ Available in full product

Architecture Guide

Chapter 5
πŸ”’ Available in full product

Deployment Guide

You’ve reached the end of the free preview

Get the full React SaaS Template and unlock everything.

All Chapters

Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.

Full Tool Suite

Access all interactive tools with complete data, all workload profiles, and the full scenario library.

Source Files

Downloadable source code, configuration files, and working examples from every chapter.

Lifetime Updates

Free updates for life. Every new chapter, tool, and improvement included.

Buy Now — $59 →
πŸ“¦ Free sample included — download another copy or visit the store for the full product.
React SaaS Template v1.0.0 β€” Free Preview