Frontend Architecture
Purpose
Structure a frontend so that a new engineer can find the code for a feature in under a minute, and so that changing one feature does not require touching five others.
When to Use
- Starting a new frontend application.
- A codebase where features are scattered across
components/, utils/, hooks/, and types/.
- Deciding where state belongs and what owns data fetching.
- Setting up a monorepo or splitting a large application.
Capabilities
- Feature-based organization and module boundaries.
- State layering: server cache, global client state, feature state, component state.
- Data-access layer design and API client structure.
- Build configuration, code splitting, and bundle budgets.
- Monorepo structure and shared package design.
Inputs
- The application's feature set and team structure.
- Current pain: where changes ripple, what is hard to find.
- Build tooling and deployment target.
Outputs
- A folder structure organized by feature, not by file type.
- Explicit rules for what may import what.
- Bundle budgets enforced in CI.
Workflow
- Organize by feature, not by kind —
features/checkout/ containing its components, hooks, api, and types beats components/, hooks/, api/ each containing a slice of every feature.
- Draw the import rules — Features may import from
shared/. Features may not import from each other; if two need the same thing, it moves to shared/. Enforce this with a lint rule, not a convention document.
- Layer the state deliberately — Server data in a query cache. Genuinely global client state (theme, session) in one store. Everything else stays local. Most "global state" is server state in disguise.
- Centralize the API client — One place that knows about base URLs, auth headers, error mapping, and retries. Feature code calls typed functions, not
fetch.
- Budget the bundle — Set a size limit per route and fail the build when it is exceeded. Bundle size regresses one dependency at a time.
Best Practices
- A
utils/ folder is where code goes to be forgotten. If a function belongs to a feature, keep it in the feature.
- Barrel files (
index.ts re-exporting everything) defeat tree-shaking and create import cycles. Import from the source module.
- Route-level code splitting is nearly free and pays for itself immediately. Component-level splitting rarely does.
- Do not put server data in a global store. It has staleness, refetch, and error semantics that a store does not model — you will rebuild a query library, badly.
- A shared component library inside the app is fine. Extracting it into a package before a second consumer exists is premature.
- Keep the dependency count low. Every dependency is bundle weight, a supply-chain risk, and a future migration.
Examples
Feature-based structure with enforced boundaries:
src/
app/ # routing, providers, global layout
features/
checkout/
components/ # only used by checkout
api/ # checkout endpoints, typed
model/ # checkout state and domain types
index.ts # the feature's public surface
orders/
account/
shared/
ui/ # design-system primitives
api/ # http client, auth, error mapping
lib/ # genuinely cross-cutting helpers
// eslint.config.js — the boundary is enforced, not merely documented.
{
rules: {
"import/no-restricted-paths": ["error", {
zones: [{
target: "./src/features/*",
from: "./src/features/*",
message: "Features must not import each other. Move shared code to src/shared.",
}],
}],
},
}
A bundle budget that fails the build:
{
"bundlesize": [
{ "path": "dist/assets/index-*.js", "maxSize": "180 kB", "compression": "brotli" },
{ "path": "dist/assets/checkout-*.js", "maxSize": "90 kB", "compression": "brotli" }
]
}
Notes
- The single most effective architectural rule in a frontend codebase is "features may not import each other". It is trivially enforceable and prevents the coupling that makes large frontends unchangeable.
- Analyze the bundle before optimizing it.
vite-bundle-visualizer or source-map-explorer will usually show one date library or one icon set accounting for a third of the payload.
- Monorepos solve a versioning problem between packages. If you have one application, a monorepo is overhead with no corresponding benefit.
1---2name: frontend-architecture3description: Use when structuring a frontend codebase. Covers module and folder organization, state boundaries, data-fetching layers, build configuration, and keeping a large application navigable.4---56# Frontend Architecture78## Purpose910Structure a frontend so that a new engineer can find the code for a feature in under a minute, and so that changing one feature does not require touching five others.1112## When to Use1314- Starting a new frontend application.15- A codebase where features are scattered across `components/`, `utils/`, `hooks/`, and `types/`.16- Deciding where state belongs and what owns data fetching.17- Setting up a monorepo or splitting a large application.1819## Capabilities2021- Feature-based organization and module boundaries.22- State layering: server cache, global client state, feature state, component state.23- Data-access layer design and API client structure.24- Build configuration, code splitting, and bundle budgets.25- Monorepo structure and shared package design.2627## Inputs2829- The application's feature set and team structure.30- Current pain: where changes ripple, what is hard to find.31- Build tooling and deployment target.3233## Outputs3435- A folder structure organized by feature, not by file type.36- Explicit rules for what may import what.37- Bundle budgets enforced in CI.3839## Workflow40411. **Organize by feature, not by kind** — `features/checkout/` containing its components, hooks, api, and types beats `components/`, `hooks/`, `api/` each containing a slice of every feature.422. **Draw the import rules** — Features may import from `shared/`. Features may not import from each other; if two need the same thing, it moves to `shared/`. Enforce this with a lint rule, not a convention document.433. **Layer the state deliberately** — Server data in a query cache. Genuinely global client state (theme, session) in one store. Everything else stays local. Most "global state" is server state in disguise.444. **Centralize the API client** — One place that knows about base URLs, auth headers, error mapping, and retries. Feature code calls typed functions, not `fetch`.455. **Budget the bundle** — Set a size limit per route and fail the build when it is exceeded. Bundle size regresses one dependency at a time.4647## Best Practices4849- A `utils/` folder is where code goes to be forgotten. If a function belongs to a feature, keep it in the feature.50- Barrel files (`index.ts` re-exporting everything) defeat tree-shaking and create import cycles. Import from the source module.51- Route-level code splitting is nearly free and pays for itself immediately. Component-level splitting rarely does.52- Do not put server data in a global store. It has staleness, refetch, and error semantics that a store does not model — you will rebuild a query library, badly.53- A shared component library inside the app is fine. Extracting it into a package before a second consumer exists is premature.54- Keep the dependency count low. Every dependency is bundle weight, a supply-chain risk, and a future migration.5556## Examples5758**Feature-based structure with enforced boundaries:**5960```text61src/62 app/ # routing, providers, global layout63 features/64 checkout/65 components/ # only used by checkout66 api/ # checkout endpoints, typed67 model/ # checkout state and domain types68 index.ts # the feature's public surface69 orders/70 account/71 shared/72 ui/ # design-system primitives73 api/ # http client, auth, error mapping74 lib/ # genuinely cross-cutting helpers75```7677```javascript78// eslint.config.js — the boundary is enforced, not merely documented.79{80 rules: {81 "import/no-restricted-paths": ["error", {82 zones: [{83 target: "./src/features/*",84 from: "./src/features/*",85 message: "Features must not import each other. Move shared code to src/shared.",86 }],87 }],88 },89}90```9192**A bundle budget that fails the build:**9394```json95{96 "bundlesize": [97 { "path": "dist/assets/index-*.js", "maxSize": "180 kB", "compression": "brotli" },98 { "path": "dist/assets/checkout-*.js", "maxSize": "90 kB", "compression": "brotli" }99 ]100}101```102103## Notes104105- The single most effective architectural rule in a frontend codebase is "features may not import each other". It is trivially enforceable and prevents the coupling that makes large frontends unchangeable.106- Analyze the bundle before optimizing it. `vite-bundle-visualizer` or `source-map-explorer` will usually show one date library or one icon set accounting for a third of the payload.107- Monorepos solve a versioning problem between packages. If you have one application, a monorepo is overhead with no corresponding benefit.