Most frontend starters solve only one use case: a single brand, basic routing, and direct API calls scattered across pages.
I wanted a stronger foundation for real projects, especially white-label and multi-tenant apps where each client needs different branding without rewriting the entire codebase.
This post explains how I built a multi-client React boilerplate with:
- React + TypeScript + Vite
- Material UI (MUI) + Tailwind
- TanStack Query
- Centralized auth and API layer
- Global error snackbar
- Client-specific build output
GitHub Repo
View the full source code on GitHub
Why This Boilerplate
The main goal was to make the codebase scalable before feature work starts:
- Keep product logic shared.
- Swap client branding/layout safely.
- Avoid duplicated API/auth/error code in every page.
- Produce build artifacts containing only the selected client modules.
This setup reduces maintenance overhead as new clients and pages are added.
1) Client-Specific Build Strategy
Initially, dynamic imports for client modules caused multiple client files to be bundled together.
To fix that, I switched to compile-time client resolution through Vite aliasing:
@clientresolves tosrc/clients/<selected-client>- Client is selected from
VITE_ORG_NAME - Build fails fast if the client folder is missing
This gives two key benefits:
- No per-file alias maintenance for new pages/components
- Build contains only the selected client theme/layout modules
2) TanStack Query for API State
TanStack Query was added to handle async workflows consistently instead of manual loading/error plumbing.
For login:
useMutationhandles submit flowisPendingdrives submit button loading state- Success stores token via auth service and redirects
- Errors are handled globally (not page-specific UI logic)
I also configured default QueryClient options for better production behavior:
- Query retry:
1 - Mutation retry:
0 staleTime:30sgcTime:5minrefetchOnWindowFocus:falserefetchOnReconnect:true
3) Shared API Client with Interceptors
Instead of calling fetch/axios directly in pages, I added a shared API client:
- One axios instance
- Request interceptor injects
Authorizationheader from auth service token - Response interceptor handles
401by clearing session and redirecting to/login
This makes auth-related HTTP behavior consistent across the app.
4) Auth Abstraction
I replaced direct localStorage usage with:
authServicefor token/session operationsuseAuthhook for UI integration
Wrappers and pages now depend on auth abstractions, not storage details.
That makes auth flow easier to evolve later (refresh tokens, cookies, secure storage, etc.).
5) Global Error Snackbar
A common issue in growing apps: every page adds its own error state + alert/snackbar component.
I solved this with a global pattern:
- Query and mutation
onErrorcallbacks publish errors - A single app-level
AppSnackbarlistens and displays queued messages - Optional
meta.suppressGlobalErrorcan disable the global toast for specific cases
Result: pages stay focused on success flow and feature logic.
6) Login Flow Design
Current login supports two modes:
- Real backend mode when
VITE_API_BASE_URLexists (POST /auth/login) - Local fallback mock mode for boilerplate/demo development
This keeps local development fast while preserving production-ready structure.
Project Structure Highlights
src/
clients/
default/
client1/
lib/
api-client.ts
services/
auth.service.ts
hooks/
useAuth.ts
components/
AppSnackbar.tsx
utils/
error-snackbar.tsKey Outcome
This boilerplate is now opinionated in the right places:
- shared logic stays centralized
- client customization stays isolated
- error handling is global and consistent
- API/auth state is scalable
It gives me a strong base to build production apps faster, especially for multi-client requirements.
What I’d Improve Next
If I continue this project, next steps would be:
- Add env validation (zod) for required runtime config.
- Add route-level error boundaries.
- Add test baseline (auth wrapper tests, login mutation integration test).
- Add refresh-token flow and silent session recovery.
If you’re building white-label apps, this architecture can save a lot of refactor time later.