One of the most common struggles in React development isn't the code itself — it's the project structure. As applications grow, an ad-hoc folder layout becomes a liability: you can't find things, dependencies become tangled, and the codebase resists change. Bulletproof React is an opinionated approach to solving this problem.
The Bulletproof React project by Alan Alickovic is a production-ready template and guide that distils battle-tested patterns for structuring large React applications. This post walks through the core ideas.
The Core Philosophy
Bulletproof React is built on a straightforward principle: organise code by feature, not by type. Instead of grouping all components together, all hooks together, and all services together, you group everything related to a feature in one place. This makes it easier to find, modify, and eventually remove features without touching unrelated code.
The goal is not the most clever architecture — it's the one that your team can navigate at 3am when something is on fire.
The Folder Structure
The top-level structure looks like this:
src/
├── app/ # App initialisation, providers, routing
├── assets/ # Static files (images, fonts, icons)
├── components/ # Shared, reusable UI components
├── config/ # Environment variables, global config
├── features/ # Feature-based modules (the core)
├── hooks/ # Shared custom hooks
├── lib/ # Third-party library configurations
├── providers/ # App-wide context providers
├── routes/ # Route definitions
├── stores/ # Global state management
├── test/ # Test utilities and setup
├── types/ # Global TypeScript types
└── utils/ # Shared utility functions
The key is the features/ directory. Each feature is a self-contained module:
features/
└── auth/
├── api/ # API call functions for this feature
├── components/ # Components specific to auth
├── hooks/ # Hooks specific to auth
├── stores/ # Local state for auth
├── types/ # Types for auth entities
└── index.ts # Public API — exports only what's needed
The Feature Module Pattern
Each feature exports a clean public API through its index.ts barrel file. Other parts of the app only import from this barrel — never from internal paths within the feature. This enforces encapsulation and makes it easy to understand a feature's boundaries.
// features/auth/index.ts
export { LoginForm } from './components/LoginForm'
export { useAuth } from './hooks/useAuth'
export type { AuthUser } from './types'
// Correct — importing through the public API
import { LoginForm } from '@/features/auth'
// Incorrect — reaching into internals
import { LoginForm } from '@/features/auth/components/LoginForm'
Shared vs Feature Components
A common source of confusion is deciding where a component lives. The rule is simple:
src/components/— used in two or more features, generic, no feature-specific logicfeatures/x/components/— specific to featurex, may depend on feature state or types
If a component starts life in a feature but later needs to be reused, promote it to src/components/. Don't pre-optimise — start feature-local and extract when needed.
API Layer
Each feature has its own api/ folder with functions that handle HTTP requests for that feature. These typically use a shared lib/axios.ts or similar base client, but the feature-specific logic lives with the feature.
// features/auth/api/login.ts
import { axios } from '@/lib/axios'
import type { AuthUser } from '../types'
export const loginWithCredentials = (
credentials: { email: string; password: string }
): Promise<AuthUser> => {
return axios.post('/auth/login', credentials)
}
Key Design Principles
Unidirectional data flow
Features should not import from each other. If feature A needs data from feature B, that data should come via shared stores, props, or the routing layer. This prevents circular dependencies and keeps the dependency graph clean.
Absolute imports
Configure path aliases (e.g. @/ pointing to src/) so imports are readable regardless of file depth. This avoids brittle relative paths like ../../../components/Button.
Colocation
Test files live next to the files they test. Styles live next to components. Types live next to the code that uses them. Everything that changes together is kept together.
When to Use This Structure
This architecture shines in medium-to-large applications with multiple features and multiple developers. For small projects or prototypes, it may be overkill — a flatter structure works fine. The right time to adopt it is when you find yourself struggling to locate code, or when adding a new feature risks breaking an existing one.
The Bulletproof React template is worth exploring directly — it includes a working example with React Query, Zustand, React Hook Form, and more, demonstrating all these patterns in context.
Tags