🛠️ Building a Multi-Client React Boilerplate with MUI and TanStack Query

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:

  1. Keep product logic shared.
  2. Swap client branding/layout safely.
  3. Avoid duplicated API/auth/error code in every page.
  4. 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:

  • @client resolves to src/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:

  • useMutation handles submit flow
  • isPending drives 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: 30s
  • gcTime: 5min
  • refetchOnWindowFocus: false
  • refetchOnReconnect: 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 Authorization header from auth service token
  • Response interceptor handles 401 by 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:

  • authService for token/session operations
  • useAuth hook 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 onError callbacks publish errors
  • A single app-level AppSnackbar listens and displays queued messages
  • Optional meta.suppressGlobalError can 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_URL exists (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.ts

Key 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:

  1. Add env validation (zod) for required runtime config.
  2. Add route-level error boundaries.
  3. Add test baseline (auth wrapper tests, login mutation integration test).
  4. 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.

Read more blogs
© Prathamesh