Building a Scalable Frontend Monorepo: Architecture Best Practices
Introduction
In modern frontend development, managing multiple applications and shared packages efficiently is crucial for maintaining code quality, enforcing consistency, and accelerating development velocity. This article introduces a production-ready monorepo architecture that demonstrates best practices for structuring large-scale frontend projects.
This Monorepo UI Starter is built with cutting-edge tools and follows industry-standard patterns to solve common challenges in enterprise frontend development:
- π Code Sharing: Reusable components, utilities, and configurations across multiple apps
- ποΈ Scalability: Clear separation of concerns with a modular package structure
- β‘ Performance: Optimized builds with caching and parallel execution via Turborepo
- π¨ Developer Experience: Integrated Storybook for component development and documentation
- π¦ Type Safety: End-to-end TypeScript with shared type definitions
- π§ Maintainability: Consistent linting, formatting, and build configurations
Key Features
Modern Stack
- React 19 with React Compiler for automatic optimization
- Vite 7 for lightning-fast builds and HMR
- TypeScript 5.9 with strict type checking
- Tailwind CSS v4 for utility-first styling
Complete Form System
- TanStack Form integration with type-safe form state management
- Pre-built form components (TextField, SelectField, SubmitButton)
- Lazy loading for optimal bundle size
- Automatic validation and error handling
Data Management
- TanStack Query for server state management
- Separate packages for queries and mutations
- Automatic caching and background refetching
- Optimistic updates support
Type-Safe Routing
- TanStack Router with file-based routing
- Auto-generated route tree
- Protected route groups
- Type-safe navigation and params
Component Library
- Mantine v8 + Tailwind v4 hybrid approach
- Atomic design structure (atoms, molecules, templates)
- Storybook for component documentation
- Phosphor Icons for consistent iconography
Developer Tools
- Turborepo for build orchestration
- pnpm for fast, efficient package management
- ESLint and Prettier for code quality
- Type checking across all packages
Technology Stack
Core Tools
- Turborepo: High-performance build system for JavaScript/TypeScript monorepos (v2.6.1)
- pnpm: Fast, disk space efficient package manager with excellent monorepo support (v9.0.0)
- React 19: Latest version with React Compiler support (v19.2.0)
- Vite 7: Next-generation frontend build tool (v7.2.4)
- TypeScript 5.9: Strict type checking across the entire codebase (v5.9.3)
UI & Styling
- Mantine: Comprehensive React component library (v8.3.8)
- Tailwind CSS v4: Utility-first CSS framework (v4.1.17)
- Storybook: Component development and documentation environment (v10.0.8)
- Phosphor Icons: Flexible icon family (v2.1.10)
Application Framework
- TanStack Router: Type-safe routing with automatic route generation (v1.139.1)
- TanStack Query: Powerful asynchronous state management (v5.90.10)
- TanStack Form: Headless, type-safe form state management (v1.26.0)
- Axios: HTTP client for API requests (v1.13.2)
- Zod: TypeScript-first schema validation (v4.1.12)
Utility Libraries
- tailwind-merge: Efficiently merge Tailwind CSS classes (v3.4.0)
- tailwind-variants: Create variant-based component styles (v3.2.2)
- @uidotdev/usehooks: Collection of modern React hooks (v2.4.1)
Monorepo Architecture
High-Level Structure
monorepo-ui-starter/ βββ apps/ # Deployable applications β βββ web/ # Main web application β βββ storybook/ # Component documentation βββ packages/ # Shared internal packages β βββ ui/ # Component library β βββ api-client/ # API layer β βββ hooks/ # React hooks & queries β βββ types/ # TypeScript definitions β βββ utils/ # Utility functions β βββ eslint-config/ # Shared ESLint configs β βββ typescript-config/ # Shared TypeScript configs βββ package.json # Root workspace configuration βββ pnpm-workspace.yaml # pnpm workspace definition βββ turbo.json # Turborepo pipeline configuration Design Philosophy
This architecture follows the "apps and packages" pattern, which separates:
- Applications (
apps/): Deployable, user-facing applications - Packages (
packages/): Reusable internal libraries and configurations
This separation ensures clear boundaries and enables:
- Independent deployment of applications
- Shared code without duplication
- Incremental adoption of shared packages
- Clear dependency graphs
Deep Dive: Package Structure
1. apps/web - Main Application
The primary React application built with Vite and modern routing patterns.
Key Features:
- File-based routing with TanStack Router
- Route generation via
@tanstack/router-plugin - Authentication context with protected routes
- Layout system for consistent UI structure
- Feature-based organization for scalability
Directory Structure:
web/ βββ src/ β βββ main.tsx # App entry point β βββ routeTree.gen.ts # Auto-generated routes β βββ components/ β β βββ layouts/ # Layout components β β βββ MainLayout/ β βββ context/ β β βββ auth.tsx # Auth state management β βββ features/ # Feature modules β β βββ Home/ β β βββ Login/ β β βββ Users/ β β βββ AddUser/ β β βββ UserDetail/ β βββ hooks/ # App-specific hooks β βββ routes/ # Route definitions β β βββ __root.tsx β β βββ (auth)/ # Auth route group β β β βββ login.tsx β β βββ (main)/ # Protected route group β β βββ index.tsx β β βββ route.tsx β β βββ users/ β β βββ index.tsx # /users β β βββ create.tsx # /users/create β β βββ $userId.tsx # /users/:userId β βββ styles/ β βββ globals.css βββ package.json βββ vite.config.ts Best Practices Demonstrated:
- Feature-Based Organization: Each feature (Home, Users, Login) is self-contained with its own components and logic
- Route Grouping: Using
(auth)and(main)for logical separation without affecting URLs - Type-Safe Routing: TanStack Router provides full TypeScript support
- Context Separation: Auth logic separated from components
2. apps/storybook - Component Documentation
Dedicated Storybook instance for developing and documenting the UI component library.
Purpose:
- Visual component testing
- Interactive documentation
- Design system showcase
- Isolated development environment
Key Files:
storybook/ βββ src/ β βββ examples/ # Example implementations β β βββ Icons.tsx β β βββ table.tsx β βββ stories/ # Component stories β βββ Button.stories.ts β βββ Input.stories.ts β βββ TextInput.stories.ts β βββ Table.stories.ts β βββ EmptyState.stories.tsx β βββ Icons.stories.ts βββ package.json 3. packages/ui - Component Library
The heart of the design system, providing reusable React components.
Architecture Highlights:
- Atomic Design Pattern: Organized into atoms, molecules, organisms, and templates
- Polymorphic Components: Using Mantine's
createPolymorphicComponentfor flexibility - Tailwind + Mantine Hybrid: Leverages both utility classes and component library
- CSS Export: Exports
globals.cssfor consistent styling
Structure:
ui/ βββ src/ β βββ globals.css # Global styles β βββ assets/ β β βββ images/ β βββ atoms/ # Basic building blocks β β βββ ActionIcon.tsx β β βββ Button.tsx β β βββ Input.tsx β β βββ TextInput.tsx β β βββ Select.tsx β β βββ Table.tsx β β βββ Icon.tsx β β βββ EmptyState.tsx β β βββ CopyButton.tsx β β βββ UIProvider.tsx β β βββ index.ts β βββ molecules/ # Composite components β β βββ Form/ # TanStack Form integration β β β βββ components/ β β β β βββ FormContainer.tsx β β β β βββ FormTextField.tsx β β β β βββ FormSelectField.tsx β β β β βββ SubmitButton.tsx β β β β βββ index.ts β β β βββ context.tsx β β β βββ index.ts β β βββ index.ts β βββ templates/ # Page templates βββ package.json Component Example:
// Wrapping Mantine with custom styling import { Button as MButton, type ButtonProps as MButtonProps } from '@mantine/core'; const Button = createPolymorphicComponent<'button', ButtonProps>( forwardRef<HTMLButtonElement, ButtonProps>(({ ...props }, ref) => ( <MButton {...props} ref={ref} classNames={{ root: 'focus:ring-2 focus:ring-brand-400', section: '[&_svg]:text-brand-300', }} /> )) ); Benefits:
- Consistent component API across the application
- Easy customization through Tailwind classes
- Type-safe props with TypeScript
- Accessible components out of the box (Mantine)
Form System with TanStack Form:
The UI package includes a sophisticated form system built on TanStack Form v1.26.0, providing type-safe, headless form state management:
// Form hook creation with field and form components import {lazy} from "react"; import {createFormHook} from "@tanstack/react-form"; const TextField = lazy(() => import('./FormTextField')); const SelectField = lazy(() => import('./FormSelectField')); const SubmitButton = lazy(() => import('./SubmitButton')) export const {useAppForm, withForm, withFieldGroup} = createFormHook({ fieldComponents: { TextField, SelectField }, formComponents: { SubmitButton, }, fieldContext, formContext, }); Form Components:
-
FormContainer: Wrapper component for form layout -
FormTextField: Text input field with validation -
FormSelectField: Select dropdown field -
SubmitButton: Form submission button with loading state
This approach provides:
- Lazy loading for optimal bundle size
- Type-safe field definitions
- Automatic validation and error handling
- Consistent form behavior across the application
4. packages/api-client - API Layer
Centralized HTTP client with type-safe API endpoints.
Structure:
api-client/ βββ src/ β βββ index.ts # Main client export β βββ axios.ts # Axios configuration β βββ user.ts # User endpoints β βββ product.ts # Product endpoints βββ package.json Key Concepts:
- Client Configuration: Single source for base URL, auth tokens, and interceptors
- Resource Organization: Each domain (user, product) has its own file
- Type Integration: Uses shared types from
@repo/types - Token Management: Integrates with utility functions for auth
Benefits:
- Centralized API logic
- Easy to mock for testing
- Consistent error handling
- Reusable across multiple apps
5. packages/hooks - React Hooks & Queries
Custom hooks and TanStack Query integration layer.
Structure:
hooks/ βββ src/ β βββ lib/ # Utility hooks β β βββ api.ts β β βββ index.ts β βββ queries/ # TanStack Query hooks β β βββ index.ts β β βββ user.ts β β βββ product.ts β βββ mutations/ # TanStack Query mutations β βββ index.ts β βββ user.ts βββ package.json Multi-path Export Strategy:
{ "exports": { "./lib": "./src/lib/index.ts", "./queries": "./src/queries/index.ts", "./mutations": "./src/mutations/index.ts" } } Benefits:
- Separation of general hooks from data-fetching hooks
- Server state management with TanStack Query
- Consistent data fetching patterns (queries) and mutation patterns
- Automatic caching and refetching
- Optimistic updates and error handling for mutations
6. packages/types - Type Definitions
Shared TypeScript interfaces and types.
Structure:
types/ βββ src/ β βββ index.ts # Main exports β βββ user.ts # User types β βββ product.ts # Product types β βββ response.ts # API response types βββ package.json Best Practices:
- Domain-based type organization
- Export both types and validators
- Single source of truth for data structures
7. packages/utils - Utility Functions
Pure utility functions for common operations.
Structure:
utils/ βββ src/ β βββ index.ts β βββ cookies.ts # Cookie management βββ package.json Purpose:
- Framework-agnostic utilities
- Shared business logic
- Helper functions
- No external dependencies (when possible)
8. Configuration Packages
packages/eslint-config
Shared ESLint configurations for consistent code quality.
eslint-config/ βββ eslint.base.js # Base config βββ eslint.react.js # React-specific rules βββ package.json Benefits:
- Consistent linting across all packages
- Easy to update rules globally
- Different configs for different package types
packages/typescript-config
Shared TypeScript configurations.
typescript-config/ βββ tsconfig.base.json # Base config βββ tsconfig.vite.json # Vite-specific config βββ package.json Configuration Inheritance:
// In app's tsconfig.json { "extends": "@repo/typescript-config/tsconfig.vite.json", "compilerOptions": { // App-specific overrides } } Workspace Configuration
pnpm Workspace
pnpm-workspace.yaml:
packages: - "apps/*" - "packages/*" This simple configuration tells pnpm to treat all directories under apps/ and packages/ as workspace packages.
Benefits:
- Single
node_modulesat root (with proper hoisting) - Fast installation with content-addressable storage
- Strict dependency management
- Workspace protocol for internal packages
Turborepo Pipeline
turbo.json:
{ "$schema": "https://turborepo.com/schema.json", "ui": "tui", "tasks": { "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**"] }, "lint": { "dependsOn": ["^lint"] }, "check-types": { "dependsOn": ["^check-types"] }, "dev": { "cache": false, "persistent": true } } } Key Concepts:
- Task Dependencies:
^buildmeans "run build in dependencies first" - Caching: Automatic caching of build outputs for fast rebuilds
- Parallel Execution: Independent tasks run in parallel
- Incremental Builds: Only rebuild changed packages
Workflow Example:
# Build everything (with dependency order) pnpm build # Build only web app and its dependencies pnpm build --filter=web # Develop specific app pnpm dev --filter=storybook Dependency Management Strategy
Workspace Dependencies
Internal packages reference each other using the workspace:* protocol:
{ "dependencies": { "@repo/ui": "workspace:*", "@repo/hooks": "workspace:*", "@repo/types": "workspace:*" } } Benefits:
- Always uses local version during development
- Prevents version mismatches
- Clear indication of internal dependencies
Peer Dependencies
UI components declare React as a peer dependency:
{ "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } } Why:
- Ensures single React instance across the app
- Prevents duplicate React in bundle
- Consumer controls the React version
Version Synchronization
Common dependencies use the same version across packages:
-
typescript:~5.9.3(all packages) -
eslint:^9.39.1(all packages) -
reactandreact-dom:^19.2.0(web + storybook) -
@tanstack/react-query:^5.90.10(web + hooks) -
@tanstack/react-form:^1.26.0(web + ui) -
vite:^7.2.4(web + storybook + ui) -
tailwindcss:^4.1.17(web + ui) -
@mantine/core:^8.3.8(ui)
Routing Architecture
TanStack Router Features
The web app uses TanStack Router with advanced features:
1. File-Based Routing
routes/ βββ __root.tsx # Root layout βββ (auth)/ β βββ login.tsx # /login βββ (main)/ βββ route.tsx # Layout for protected routes βββ index.tsx # / βββ users/ βββ index.tsx # /users βββ $userId.tsx # /users/:userId 2. Route Context
export interface AppRouterContext { queryClient: QueryClient, auth: AuthContext } const router = createRouter({ routeTree, context: { queryClient, auth } }); 3. Type-Safe Navigation
// Auto-generated types for all routes import { Link } from '@tanstack/react-router' <Link to="/users/$userId" params={{ userId: '123' }} /> State Management Patterns
1. Server State (TanStack Query)
For data from APIs:
// In @repo/hooks/queries/user.ts export const useUsers = () => { return useQuery({ queryKey: ['users'], queryFn: () => apiClient.getUsers() }); }; For data mutations:
// In @repo/hooks/mutations/user.ts export const useCreateUser = () => { return useMutation({ mutationFn: (data: CreateUserData) => apiClient.users.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); } }); }; Benefits:
- Automatic caching
- Background refetching
- Optimistic updates
- Loading/error states
- Automatic query invalidation after mutations
2. Authentication State (Context)
For global auth state:
// In apps/web/src/context/auth.tsx export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); // Auth logic return <AuthContext.Provider value={{ user }} />; }; 3. Local State (React Hooks)
For component-specific state:
const [isOpen, setIsOpen] = useState(false); Development Workflow
Getting Started
# Install dependencies pnpm install # Develop all apps pnpm dev # Develop specific app pnpm dev --filter=web pnpm dev --filter=storybook # Build all packages pnpm build # Lint everything pnpm lint # Type check all packages pnpm check-types # Format code pnpm format Adding a New Package
- Create package directory:
mkdir packages/new-package - Add
package.json:
{ "name": "@repo/new-package", "private": true, "version": "0.0.0", "type": "module", "exports": { ".": "./src/index.ts" } } - The package is automatically detected by pnpm workspace
Adding a New App
Similar to package, but in apps/ directory:
mkdir apps/new-app # Add package.json with appropriate dependencies Best Practices & Patterns
1. Package Naming Convention
- Use
@repo/scope for all internal packages - Descriptive names:
@repo/ui,@repo/api-client,@repo/hooks - Consistent with standard naming (e.g.,
eslint-config,typescript-config)
2. Export Strategy
Named Exports for Atoms:
// @repo/ui/atoms/index.ts export { default as Button } from './Button'; export { default as Input } from './Input'; export { default as TextInput } from './TextInput'; export { default as Select } from './Select'; export { default as Table } from './Table'; export { default as EmptyState } from './EmptyState'; export { default as Icon } from './Icon'; export * from './Icon'; Sub-path Exports for Categories:
{ "exports": { "./globals.css": "./src/globals.css", "./atoms": "./src/atoms/index.ts", "./molecules": "./src/molecules/index.ts" } } Usage in Applications:
// Import atoms import { Button, Input, Select } from '@repo/ui/atoms'; // Import molecules (Form components) import { FormContainer, useAppForm } from '@repo/ui/molecules'; // Import global styles import '@repo/ui/globals.css'; 3. Type Safety
- Strict TypeScript in all packages
- Shared types in
@repo/types - Runtime validation with Zod where needed
- Proper peer dependencies for React components
4. Code Organization
Feature-Based Structure in Apps:
features/ βββ Users/ β βββ Users.tsx β βββ UserList.tsx β βββ UserCard.tsx β βββ index.ts Atomic Design in UI Package:
ui/ βββ atoms/ # Button, Input βββ molecules/ # SearchBar, Card βββ organisms/ # Header, Sidebar βββ templates/ # PageLayout 5. Documentation
- README in each package explaining its purpose
- Storybook for visual components
- JSDoc comments for complex functions
- TypeScript types as inline documentation
6. Performance Optimization
- Tree-shaking: ES modules for optimal bundling
- Code splitting: Dynamic imports for routes
- Caching: Turborepo caches all build outputs
- React Compiler: Enabled for automatic optimization
7. Testing Strategy
While not shown in current structure, recommended additions:
packages/ui/ βββ src/ β βββ atoms/ β βββ Button.tsx β βββ Button.test.tsx βββ vitest.config.ts Scaling Considerations
When to Create New Packages
Create a new package when:
- β Code is reused across 2+ apps
- β It has a clear, single responsibility
- β It can be developed/tested independently
- β It has stable interfaces
Don't create a package if:
- β It's only used in one app
- β It's tightly coupled to app logic
- β The API changes frequently
Adding More Apps
This structure easily supports:
- Admin dashboard (
apps/admin) - Mobile web app (
apps/mobile) - Landing page (
apps/landing) - Internal tools (
apps/tools)
All sharing the same packages for consistency.
Multi-Framework Support
While this starter uses React, you could add:
- Vue app using
@repo/typesand@repo/api-client - Node.js backend using
@repo/types - React Native app using
@repo/hooksand@repo/utils
Migration Path for Existing Projects
Step 1: Monorepo Setup
- Add
pnpm-workspace.yaml - Add
turbo.json - Update root
package.json
Step 2: Extract Shared Code
- Move shared components to
packages/ui - Move API calls to
packages/api-client - Move types to
packages/types
Step 3: Configure Dependencies
- Update imports to use
@repo/* - Add workspace dependencies
- Test builds and dev mode
Step 4: Optimize
- Configure Turborepo caching
- Set up parallel execution
- Add Storybook for components
Common Pitfalls & Solutions
Issue: Circular Dependencies
Solution: Use dependency injection and proper layering
utils β types β api-client β hooks β ui β apps Issue: Slow Installations
Solution: Use pnpm and Turborepo caching
# pnpm is much faster than npm/yarn pnpm install --frozen-lockfile Issue: Type Errors Across Packages
Solution: Use project references in TypeScript
{ "references": [ { "path": "../../packages/types" } ] } Issue: Hot Reload Not Working
Solution: Configure Vite to watch workspace packages
// vite.config.ts export default { server: { watch: { ignored: ['!**/node_modules/@repo/**'] } } } Real-World Benefits
Development Speed
- β Shared components: Build once, use everywhere
- β Type safety: Catch errors at compile time
- β Fast builds: Turborepo cache eliminates redundant work
Code Quality
- β Consistent patterns: Shared configs enforce standards
- β Reusability: DRY principle across all apps
Team Collaboration
- β Clear ownership: Each package has clear boundaries
- β Independent work: Teams can work on different apps/packages
- β Shared knowledge: Common patterns across codebase
Maintainability
- β Single source of truth: Types, configs, and components
- β Easy updates: Change once, update everywhere
- β Dependency management: Workspace protocol prevents version drift
Conclusion
This monorepo architecture represents modern best practices for frontend development at scale. By combining Turborepo's build orchestration, pnpm's efficient package management, and a well-organized structure, it provides:
- Exceptional Developer Experience: Fast builds, hot reload, type safety
- Code Reusability: Shared packages eliminate duplication
- Scalability: Easy to add new apps and packages
- Maintainability: Consistent patterns and single source of truth
- Performance: Optimized builds with caching and parallel execution
Whether you're starting a new project or migrating an existing one, this architecture provides a solid foundation for building modern, scalable frontend applications.
Further Resources
- Turborepo Documentation: https://turbo.build/repo/docs
- pnpm Workspaces: https://pnpm.io/workspaces
- TanStack Router: https://tanstack.com/router/latest
- TanStack Query: https://tanstack.com/query/latest
- TanStack Form: https://tanstack.com/form/latest
- Mantine UI: https://mantine.dev/
- Tailwind CSS: https://tailwindcss.com/
- Phosphor Icons: https://phosphoricons.com/
- Vite: https://vitejs.dev/
Repository
Check out the full implementation at: [https://github.com/khanhspring/monorepo-ui-starter]
Feel free to use this starter as a foundation for your next project, and contribute improvements back to the community!
Last updated: November 27, 2025
Built with β€οΈ for the frontend community
Top comments (0)