A production-ready React starter template built with Vite, TanStack Router, TanStack Query, shadcn/ui, and Tailwind CSS v4. Includes cookie-based auth, dark mode, toast notifications, and security hardening.
Designed to pair with api-template (FastAPI backend with cookie JWT auth and refresh tokens), but can work with any backend.
- React 19 - UI library
- TypeScript - Type safety
- Vite - Build tool and dev server
- TanStack Router - Type-safe file-based routing
- TanStack Query - Server state management
- shadcn/ui - Composable component library (Button, Card, Input, Label)
- Tailwind CSS v4 - Utility-first CSS framework
- Zod v4 - TypeScript-first schema validation
- ky - HTTP client with automatic token refresh
- orval - OpenAPI client generator (React Query hooks, TypeScript types, Zod schemas, MSW mocks)
- MSW - API mocking for tests and development
- Husky - Pre-commit hooks (lint, test, build)
- Type-safe file-based routing with TanStack Router
- Cookie-based auth with automatic token refresh and request coalescing
- Dark/light/system theme with localStorage persistence
- Global mutation error handling with toast notifications
- Content Security Policy headers
- 404 not-found page and root error boundary with retry
- Auth context available in router for route guards
- Structured logging (console in dev, JSON in prod)
- Analytics provider with route tracking
- Feature flags (fetched from API, env-var fallback)
- Auto-generated API client from OpenAPI specs via orval
- Test utilities with configurable auth state
- Node.js 24+
- pnpm (recommended, but any Node package manager should work)
# Clone the repository
cd web-template
# Install dependencies
pnpm install
# (Optional) Generate API client
# Requires a running backend with an OpenAPI spec; you can skip this for now
# and run it later once your backend is up (see "API Client Generation" section).
pnpm generate-apipnpm devOpen http://localhost:5173 in your browser.
Set VITE_API_URL=http://localhost:8000 in a .env file to connect to a local backend.
pnpm buildAfter creating a project from this template, update the following:
- App name — Replace
React Modern Stackwith your app name in:index.html(page title)src/pages/home/home-page.tsx(heading and description)src/pages/home/home-page.test.tsx(test assertion)
- Package name — Update
nameinpackage.json - Content Security Policy — In
index.html, update the CSP meta tag:- Add your production API domain to
connect-src(e.g.https://api.yourapp.com) - Remove
http://localhost:*for production builds, or use environment-specific CSP - Consider replacing
'unsafe-inline'with nonce-based CSP via a Vite plugin likevite-plugin-cspfor stricter security
- Add your production API domain to
- localStorage keys (optional) — Rename the key prefixes if you want app-specific isolation:
app_themeinsrc/components/theme-provider.tsxapp_auth_emailinsrc/lib/auth.tsx
- Environment variables — See Environment Variables for
VITE_API_URLandOPENAPI_URL
The template includes a complete auth setup designed to work with the companion api-template:
- AuthProvider (
src/lib/auth.tsx) — React context trackingisAuthenticated,isLoading,email, anduserId - Automatic token refresh (
src/api/api.ts) — 401 responses trigger a refresh attempt; concurrent requests are coalesced into a single refresh call - Session check on load —
GET /auth/mevalidates the session on mount - Auth in router context —
authis available in routebeforeLoadfor route guards
- Backend sets httpOnly cookies (
app_access+app_refresh) on login - All API requests include cookies via
credentials: "include" - On 401, the client POSTs to
/auth/refreshto rotate tokens - If refresh succeeds, the original request is retried transparently
- If refresh fails,
onUnauthorizedfires and auth state is cleared
Dark/light/system theme support via ThemeProvider:
useTheme()hook for reading/setting the themeThemeTogglecomponent for cycling between modes- Persists to localStorage (key:
app_theme) - Applies
.dark/.lightclass to<html>for Tailwind dark mode
- Global mutation errors —
MutationCacheinQueryClientcatches errors from all mutations and shows a toast via sonner - Per-mutation opt-out — Set
meta: { skipGlobalError: true }on a mutation to handle errors locally getErrorMessage()(src/lib/api-errors.ts) — Extracts human-readable messages from FastAPI error responses (supportsdetailas string, array, or object)
This template uses orval to generate type-safe React Query hooks from your backend's OpenAPI specification.
# Start your backend server first, then:
pnpm generate-apiThis generates:
- React Query hooks (
useQuery/useMutation) insrc/api/generated/hooks/— with integrated Zod validation - TypeScript types for all request/response schemas in
src/api/generated/types/ - Standalone Zod schemas in
src/api/generated/zod/(for form validation, manual use) - MSW mock handlers for testing in
src/api/generated/mocks/
The orval configuration is in orval.config.ts. By default, it fetches the OpenAPI spec from http://localhost:8000/openapi.json.
For CI/CD, set the OPENAPI_URL repository variable to point to your staging/dev backend. The CI workflow will verify that generated types are up-to-date.
After generating, import hooks and schemas from src/api/generated/. The generated code is organized by API tags.
React Query hooks (with automatic Zod validation):
import { useGetNotes, useCreateNote } from "@/api/generated/hooks/notes/notes"
function NoteList() {
const { data, isLoading, error } = useGetNotes()
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <pre>{JSON.stringify(data, null, 2)}</pre>
}Standalone Zod schemas (for form validation, etc.):
import { NoteCreate } from "@/api/generated/zod/notes/notes"
const result = NoteCreate.safeParse(formData)
if (!result.success) {
console.error(result.error.issues)
}Note: The exact imports and response structures depend on your backend's OpenAPI specification. Check the generated files in
src/api/generated/after runningpnpm generate-api.
pnpm test # Watch mode
pnpm test:run # Single run
pnpm test:coverage # With coverage
pnpm test:ui # Visual UIrenderWithFileRoutes() (src/test/renderers.tsx) renders the full router with providers and configurable auth state:
import { renderWithFileRoutes } from "@/test/renderers"
// Default: authenticated as test@example.com
await renderWithFileRoutes(<div />, { initialLocation: "/dashboard" })
// Custom auth state
await renderWithFileRoutes(<div />, {
initialLocation: "/login",
routerContext: {
auth: {
isAuthenticated: false,
isLoading: false,
email: null,
userId: null,
login: () => {},
logout: async () => {},
checkAuth: async () => {},
},
},
})MSW handlers are configured in src/api/handlers.ts. A default handler for /auth/me (returns 401) is included to suppress warnings during tests.
src/
├── api/
│ ├── api.ts # Ky client with token refresh
│ ├── orval-client.ts # Custom adapter for orval (uses ky)
│ ├── handlers.ts # MSW handlers aggregator
│ └── generated/ # Auto-generated (do not edit)
│ ├── hooks/ # React Query hooks
│ ├── types/ # TypeScript types
│ ├── zod/ # Zod schemas
│ └── mocks/ # MSW mock handlers
├── components/
│ ├── ui/ # shadcn/ui components (Button, Card, Input, Label)
│ ├── theme-provider.tsx # Dark/light/system theme context
│ ├── theme-toggle.tsx # Theme cycle button
│ ├── error-boundary.tsx # Root error component with retry
│ └── not-found.tsx # 404 page
├── lib/
│ ├── analytics.tsx # AnalyticsProvider + useAnalytics hook
│ ├── auth.tsx # AuthProvider + useAuth hook
│ ├── api-errors.ts # Error message extraction
│ ├── feature-flags.tsx # FeatureFlagProvider + useFeatureFlag hook
│ ├── logger.ts # Structured logging abstraction
│ └── utils.ts # cn() class merge helper
├── pages/ # Page components
├── routes/ # TanStack Router file-based routes
│ ├── __root.tsx # Root layout (Toaster, devtools, error/404, route tracking)
│ └── index.tsx # Home page route
├── test/
│ ├── setup.ts # Vitest + MSW setup
│ └── renderers.tsx # renderWithFileRoutes() test utility
└── main.tsx # Entry point (providers, router, mutation cache)
This template uses shadcn/ui. To add new components:
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add select
# etc.src/lib/logger.ts provides logger.debug(), logger.info(), logger.warn(), and logger.error() methods. In development, it writes to the browser console with level filtering. In production, it outputs structured JSON strings (ready to forward to any reporting service).
Set VITE_LOG_LEVEL to control the minimum level (default: debug in dev, warn in prod).
AnalyticsProvider wraps the app and exposes a useAnalytics() hook with track(), identify(), and page() methods. Route changes are tracked automatically. The default implementation logs to the logger — pass a custom backend prop to AnalyticsProvider to send events to a real service.
FeatureFlagProvider fetches flags from the API's GET /flags endpoint (cached via TanStack Query, refetches on window focus). If the API call fails, it falls back to VITE_FEATURE_* env vars.
Use the useFeatureFlag("flag_name") hook or the <Feature flag="flag_name"> component for conditional rendering.
| Variable | Description | Default |
|---|---|---|
VITE_API_URL |
Backend API URL | /api |
VITE_LOG_LEVEL |
Minimum log level (debug/info/warn/error) | debug (dev), warn (prod) |
VITE_FEATURE_* |
Feature flag overrides (e.g. VITE_FEATURE_NEW_DASHBOARD=true) |
(none) |
OPENAPI_URL |
OpenAPI spec URL (for code generation) | http://localhost:8000/openapi.json |
OPENAPI_URL is only used during development for pnpm generate-api. It is not needed in production — the generated files are committed to the repo.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.