Modern React with TypeScript is an excellent stack for product teams—until the team grows. What worked for five engineers becomes friction for fifty: inconsistent patterns, brittle types, confusing folder structures, and “clever” abstractions that new teammates can’t safely change.
This guide focuses on practices that scale across large teams: predictable architecture, opinionated typing conventions, tooling that enforces standards, and a few patterns we’ve seen reduce bugs and review time.
1) Set a clear module boundary: feature-first architecture
Large teams need boundaries. A feature-first structure typically outperforms “type-first” or “layer-first” layouts because it aligns with ownership and reduces cross-team churn.
A practical starting point:
src/app/– app shell (router, providers, bootstrap)src/features/<feature>/– the primary unit of ownershipcomponents/(feature-scoped UI)routes/(feature pages)hooks/api/(client functions for this feature)types/(feature contracts)index.ts(public exports)
src/shared/– reusable primitives (design system wrappers, utilities)
Rules that keep it clean:
- Features can import from
shared, but not from other features directly. - If something needs to be shared across features, promote it intentionally to
shared(and document why). - Enforce boundaries with ESLint rules (e.g.,
eslint-plugin-importoreslint-plugin-boundaries).
This avoids the “spaghetti imports” problem where everyone reaches into everyone else’s internals.
2) Treat TypeScript as a product constraint, not optional safety
In big codebases, TypeScript either becomes the safety net or another source of confusion. Teams that scale well standardize how types are written and where they live.
Recommended conventions:
- Prefer
typealiases for most shapes; useinterfacewhen you need declaration merging or public extensibility. - Avoid “catch-all” index signatures like
{ [key: string]: any }. They erase guarantees. - Enable strictness and keep it on:
"strict": true"noUncheckedIndexedAccess": true(forces safer map access)"exactOptionalPropertyTypes": true(prevents subtle optional bugs)
Also: don’t hide problems behind as casting. If as shows up repeatedly, it’s often a sign that your domain model is missing a discriminant, your API types are wrong, or you’re skipping runtime validation.
3) Make API contracts explicit: typed fetch + runtime validation
The fastest way to get production bugs is to assume backend responses match your TypeScript types. They don’t—especially across multiple teams.
Pattern that scales:
- Use a thin, typed API layer per feature.
- Validate responses at runtime with a schema (Zod is a common choice).
Example approach:
- Define a Zod schema for
User. - Infer the TS type from the schema (
z.infer<typeof UserSchema>). - Parse API responses (
UserSchema.parse(data)) before handing them to the app.
This single practice prevents a surprising number of “undefined is not a function” incidents and makes refactors safer.
4) Standardize component patterns: fewer options, faster delivery
Large teams suffer when there are ten ways to do the same thing. Pick a couple of patterns and enforce them in reviews.
Pragmatic component rules:
- Prefer function components with explicit props types.
- Keep components “dumb” by default; move data fetching and orchestration into route-level components or dedicated hooks.
- Don’t overuse
React.FC. It implicitly addschildrenand can hide errors.
Props typing:
- Use
type Props = { ... }directly above the component. - Prefer explicit return types only when helpful; avoid littering with
: JSX.Element.
Memoization:
- Don’t preemptively wrap everything with
useMemoanduseCallback. It adds cognitive load and often doesn’t improve performance. - Use memoization when profiling shows real wins or when passing stable references is required.
5) One state strategy per problem (and document it)
State management debates waste time. The real best practice is to be intentional and consistent.
A scalable split:
- Server state: TanStack Query (caching, retries, invalidation)
- Global UI state (theme, feature flags): a small store (Zustand) or Context
- Local UI state: component state and reducers
Key rule: don’t mirror server state into global state. Let the query cache be the source of truth. When teams copy API data into stores “for convenience,” they create hard-to-debug inconsistencies.
6) Keep routing and composition boring
As teams grow, routing becomes the integration surface. Make routes predictable:
- Put route components under
features/<feature>/routes/. - Route components compose smaller components; they don’t contain every detail.
- Lazy-load at the route boundary to keep bundles manageable.
If you’re on React Router, consider “data routers” only if your team is ready for the mental model. Otherwise, keep fetching in hooks and standardize loading/error UI.
7) Enforce standards with tooling, not tribal memory
In large teams, “we usually do X” is not a standard. Tooling is.
Baseline:
- ESLint with TypeScript rules + import/order rules
- Prettier for formatting (no bikeshedding)
tsc --noEmitin CIlint-staged+ pre-commit hooks
Add two high-leverage guardrails:
- API extractor / “public exports only” convention: forbid deep imports (
features/foo/components/Bar) and requirefeatures/fooexports viaindex.ts. - Codegen for API clients (OpenAPI/GraphQL codegen) to reduce manual typing.
CI should fail fast, and the same checks should run locally.
8) Write tests where they pay off: contracts and critical flows
A huge suite of brittle unit tests slows teams down. Prefer a testing pyramid that matches risk:
- Component/unit tests for pure logic and tricky UI states
- Integration tests around feature flows (React Testing Library)
- A small set of end-to-end tests for critical paths (Playwright)
What scales best is testing contracts and boundaries:
- API schema validation tests
- Reducers and state transitions
- Permission gating and role-based UI
Avoid snapshot tests for everything; they create noise and discourage legitimate UI change.
9) Make code review a throughput tool, not a gate
For big teams, review culture matters as much as code. A few norms that consistently improve throughput:
- Require a short PR description: intent, approach, screenshots, rollout notes.
- Keep PRs small. If a change touches many files, ask whether a boundary is missing.
- Use “suggestion” language for style nits; reserve “blocking” for correctness, safety, and architecture.
- Document decisions in an ADR (Architecture Decision Record) when patterns change.
Conclusion: scale comes from constraints
The best React + TypeScript codebases for large teams aren’t the most clever—they’re the most constrained. Feature boundaries prevent accidental coupling. Strict TypeScript plus runtime validation keeps contracts honest. Opinionated component patterns reduce review time. Tooling turns guidelines into reality.
If you do one thing this week: enforce module boundaries and standardize your API contract strategy. Those two decisions reduce cross-team friction more than any micro-optimization ever will.