Project Folder Structure Best Practices in 2026
How to structure a modern codebase — feature-based organization, a clear source root, colocation, path aliases, monorepos and keeping build output out of source — each with why it matters and an example tree.
A good folder structure makes a codebase easy to navigate, refactor and scale; a bad one scatters every feature across the tree and makes changes risky. The modern consensus has shifted decisively toward organizing by feature rather than by technical type. Here are the folder-structure best practices that hold across front-end apps, Node back ends and monorepos in 2026 — each with the reasoning and an example.
1. Organize by feature/domain, not by technical type
Type-based trees (controllers/, models/, views/) scatter one feature across the codebase; feature folders keep everything for a capability together, so changes stay local and boundaries are visible. Don’t over-engineer tiny apps — adopt feature folders once you have several distinct domains.
❌ type-based ✅ feature-based
src/ src/
components/ features/
hooks/ auth/
services/ checkout/
utils/ dashboard/
types/ shared/
2. Keep a single clear source root (src/ or app/)
A dedicated source root separates code you write from config, tooling and build output at the repo root, and makes path aliases and tooling globs simple. Be consistent — pick src/ (common in Vite-style apps) or app/ (Next.js App Router) and don’t mix both for the same purpose.
project/
src/ # all application source
public/ # static assets served as-is
tests/
package.json
tsconfig.json
3. Separate concerns inside a feature
A predictable sub-structure means anyone can find a feature’s UI, data-fetching and pure logic without hunting, and it clarifies dependency direction (UI → services → lib). Keep lib/utils for *pure* functions; anything that touches the network, storage or framework belongs in services/.
features/checkout/
components/ # UI
hooks/ # stateful logic
services/ # API / data access
lib/ # pure helpers, no framework deps
types/ # TypeScript types for this feature
index.ts # curated public surface
4. Colocate related files (component + styles + test)
Files that change together should live together, so a feature is a self-contained unit you can move, refactor or delete cleanly. In the Next.js App Router, colocation inside app/ is safe because only page/route files become routes — use a _components private folder to be explicit that a folder is non-routable.
Button/
Button.tsx
Button.module.css
Button.test.tsx
Button.stories.tsx
index.ts
5. Adopt one consistent naming convention
Mixed casing (UserCard.tsx vs user-card.tsx) breaks imports on case-sensitive filesystems (Linux CI) even when they work on macOS or Windows. Enforce a convention with ESLint (unicorn/filename-case) so it can’t drift.
✅ PascalCase for components: UserCard.tsx
✅ camelCase for hooks/utils: useAuth.ts, formatDate.ts
✅ kebab-case for route segments: app/user-settings/page.tsx
6. Use absolute imports / path aliases
../../../../utils is fragile and breaks when files move; aliases are stable, readable and refactor-safe. The catch: the alias must be configured in *every* tool that resolves modules — TypeScript, the bundler, your test runner and ESLint — or you get "works in the editor, fails at build."
// tsconfig.json
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }
import { formatDate } from "@/lib/formatDate"; // ✅
import { formatDate } from "../../../lib/formatDate"; // ❌
7. Be cautious with barrel (index.ts) files
Barrels give clean imports but can hurt tree-shaking, create circular-dependency traps and slow bundlers/HMR when a re-export pulls in an entire feature. Use small, per-feature barrels that export only the intended public surface — a single giant root barrel re-exporting everything is an anti-pattern.
// features/checkout/index.ts — export only the PUBLIC surface
export { CheckoutPage } from "./components/CheckoutPage";
export type { CartItem } from "./types";
// keep internal helpers OUT of the barrel
8. Separate environment/config from code
Config that varies per environment must be injectable, not baked into source, and secrets must stay out of the repo. Only expose client-safe values to the browser — in Next.js, only NEXT_PUBLIC_* vars reach the client, so never prefix a secret with it. Commit .env.example, git-ignore the real .env*.
config/
default.ts
production.ts
.env.local # git-ignored
.env.example # committed template, no real values
9. Place tests alongside code or in a mirror tree — consistently
Both colocated and mirrored test layouts work; the failure mode is mixing them. Colocated tests maximize discoverability; a mirrored tests/ tree keeps source directories lean. Whichever you choose, exclude test files from the production build so *.test.ts and mocks never ship.
Colocated: Mirrored:
src/features/auth/ src/features/auth/login.ts
login.ts tests/features/auth/login.test.ts
login.test.ts
10. Keep public/static assets separate from source
Static files (favicons, robots.txt, images served verbatim) are handled differently from compiled source and shouldn’t pass through the bundler. Files in public/ keep their exact name and URL (good for robots.txt); src/assets/ imports get hashed filenames for cache-busting — putting a logo in public/ forfeits optimization.
public/ # served at site root, unprocessed
favicon.ico
robots.txt
src/assets/ # imported & processed (optimized, hashed)
logo.svg
11. Structure a back-end app in layers
Separating HTTP concerns (routing, controllers) from business logic (services) from persistence (repositories) keeps logic testable and framework-agnostic. Keep req/res out of the service layer — controllers translate HTTP to plain arguments and back, so business logic stays unit-testable without a server.
src/
routes/ # HTTP route definitions
controllers/ # request/response handling only
services/ # business logic (no req/res)
repositories/ # data access
middleware/
config/
12. Use a monorepo layout (apps/ + packages/) with a workspace tool
A monorepo shares code and tooling across apps with enforced boundaries and graph-aware, cached CI — far better than copy-paste or many tiny repos. Match the tool to scale: pnpm/npm workspaces for a few packages, Turborepo + pnpm for most JS/TS repos, Nx for large orgs. Use workspace:* for internal deps so packages always resolve locally.
monorepo/
apps/
web/ # Next.js app
api/ # Node service
packages/
ui/ # shared components
config/ # eslint/tsconfig presets
types/ # shared TS types
turbo.json
pnpm-workspace.yaml
13. Extract a shared types (and config) package
A single source of truth for cross-boundary types (an API contract consumed by both web and api) prevents drift and duplicated definitions. Keep shared packages dependency-light and side-effect-free ("sideEffects": false), and set an explicit exports map so consumers only reach the intended public API.
// packages/types/src/index.ts
export interface User { id: string; email: string; }
// apps/web and apps/api both:
import type { User } from "@acme/types";
14. Avoid deep nesting; flatten where reasonable
Paths like src/features/user/components/profile/settings/tabs/general/... are hard to navigate and produce brittle imports; shallow trees are easier to reason about. A rough heuristic: if you’re past three or four levels inside a feature, that’s usually a signal to split it into a sibling feature rather than nest deeper.
❌ features/user/components/profile/tabs/general/GeneralTab.tsx
✅ features/user-profile/GeneralTab.tsx
15. Keep build output out of source control and out of src/
Generated artifacts (dist/, .next/, build/, node_modules/, coverage) are reproducible and huge — committing them bloats the repo and causes merge conflicts. Ignore them globally, and in a monorepo let the build tool cache outputs rather than committing them. Never import from dist/ in source — import from package names.
# .gitignore
node_modules/
dist/
build/
.next/
coverage/
*.log
Folder structure checklist
- Organize by feature/domain, not by technical type; keep a single clear source root.
- Separate UI, services and pure
libinside each feature; colocate related files. - Use one naming convention and absolute path aliases (configured in every tool).
- Use small per-feature barrels, not one giant root barrel.
- Separate env/config and static assets from source; keep tests consistent and out of the build.
- Layer back-end apps (routes → services → data), keeping
req/resout of services. - For multiple apps, use a monorepo (Turborepo/Nx) with shared types; avoid deep nesting.
- Keep build output out of git and out of
src/.
Frequently asked questions
Should I organize my project by feature or by file type?
components/, services/, utils/) scatter one feature across the tree, so a single change touches many directories. Feature folders keep everything for a capability together, making changes local and boundaries visible.What is the best monorepo tool in 2026?
Are barrel (index.ts) files bad?
Where should tests live?
Button.tsx + Button.test.tsx) or in a mirrored tests/ tree — both work. The important thing is to be consistent and to exclude test files from the production build so they never ship.Experiment with a clean project structure in XCODX Studio — it supports real multi-file projects with a file tree, so you can lay out features, shared libs and colocated tests and preview them live. For the code inside those folders, see our React and Node.js best practices.