Folder Structure Best Practices
A project's directory structure isn't just organization — it's living documentation of the architecture. A well-thought-out structure communicates where each thing lives, what the system's boundaries are, and which conventions the team agreed to follow. A poorly-thought-out structure hides the architecture, creates friction, and turns into a junk drawer.
Intent
Define directory conventions that make the system's architecture visible in the file structure, making navigation easier, reducing the time it takes to find where something is or should be, and making the boundaries and responsibilities of each part of the system clear.
A good folder structure answers three questions without the developer needing to open a single file: what does this project do, how is it organized, and where do I put new code.
Problem
Without explicit conventions, projects grow entropically:
- Each developer puts files wherever they find convenient.
- Code of different natures (configuration, business logic, utilities, infrastructure) gets mixed in the same directories.
- The
utils/folder becomes a repository for everything without a clear owner. - New team members take weeks to understand where to look for things.
- The actual architecture and the folder structure diverge — the code says one thing, the folder says another.
The cost isn't just cosmetic. A confusing structure creates circular imports, makes refactoring risky, and hurts visibility into what can be reused.
Structure
Common project-level conventions
Regardless of the language or framework, certain root-level directory conventions repeat across most well-organized projects:
project/
├── src/ ← application source code (the only place with logic)
├── tests/ ← automated tests (or __tests__/ next to src/)
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── docs/ ← technical documentation (ADRs, diagrams, guides)
├── config/ ← environment and tooling configuration files
│ ├── jest.config.ts
│ └── tsconfig.json
├── scripts/ ← build, seed, migration and automation scripts
└── dist/ ← compiled artifact (generated; never versioned)
Example: backend API (Node/Express with layers)
src/
├── users/ ← feature: users
│ ├── http/
│ │ ├── UserController.ts
│ │ └── CreateUserDto.ts
│ ├── application/
│ │ └── CreateUserUseCase.ts
│ ├── domain/
│ │ ├── User.ts ← domain entity
│ │ └── IUserRepository.ts ← interface (port)
│ └── infra/
│ └── PrismaUserRepository.ts ← concrete implementation
│
├── orders/ ← feature: orders
│ ├── http/
│ ├── application/
│ ├── domain/
│ └── infra/
│
├── shared/ ← genuinely shared code
│ ├── errors/ ← domain error classes
│ ├── types/ ← shared TypeScript types
│ └── middleware/ ← cross-cutting HTTP middleware
│
└── app.ts ← bootstrap: sets up the Express server
Example: frontend SPA (React/Vue)
src/
├── features/ ← application features
│ ├── auth/
│ │ ├── components/ ← auth-specific components
│ │ ├── hooks/ ← auth hooks (useAuth, useLogin)
│ │ ├── services/ ← auth API calls
│ │ └── types.ts ← auth types
│ │
│ └── dashboard/
│ ├── components/
│ ├── hooks/
│ └── services/
│
├── components/ ← reusable UI components (no business logic)
│ ├── Button/
│ │ ├── Button.tsx
│ │ └── Button.test.tsx
│ └── Modal/
│
├── hooks/ ← generic hooks (useDebounce, useFetch)
├── services/ ← HTTP client, API configuration
├── store/ ← global state (Redux, Zustand, Pinia)
├── types/ ← global TypeScript types
└── main.tsx ← entry point
How structure exposes (or hides) architecture
Screaming Architecture
Robert C. Martin proposed that a project's structure should
"scream" what the system does — not the framework it uses. A
root folder called rails/ screams "this is a Rails
project." A root folder with users/,
orders/ and payments/ screams "this is
an e-commerce system." The second reveals the business intent.
In practice, this means the top-level directories should reflect the system's features or domains, not its technical mechanisms. Frameworks and libraries are details — they should only appear in the outermost layers.
Where to put tests
There are two conventions, each with trade-offs:
-
Next to the code (
User.test.tsbesideUser.ts): makes it easy to find and keep the test next to the implementation. Favors unit tests. More common in JavaScript/TypeScript projects with Jest or Vitest. -
A separate
tests/directory: clearly separates production code from test code. Makes it easier to have integration and e2e tests that exercise multiple modules. More common in Python, Go and Java projects.
The most important thing is to be consistent. Mixing both conventions in the same project creates confusion about where something's test lives.
Practical principles
Name directories by purpose, not by type
helpers/, utils/ and misc/
describe the type of file (auxiliary code), not its
purpose. Replace them with names that communicate
intent: formatters/, validators/,
date-utils/. If you can't name a directory
precisely, the code inside it probably doesn't have enough
cohesion to be grouped together.
Maximum useful depth
Structures with more than four or five levels of depth are hard
to navigate. If a file's path is
src/features/orders/services/handlers/processors/,
the hierarchy is likely modeling subdivisions that could be
captured by the file's name instead of by directories.
Consistency over perfection
A good structure applied consistently is more valuable than a
"perfect" structure that each developer interprets differently
when deciding where things go. Document the conventions in the
README or in an ADR (Architecture Decision Record)
and follow them.
Best practices vs anti-patterns
Best practices
- Separate source code (
src/) from generated artifacts (dist/) and automation scripts (scripts/). - Lowercase, hyphenated directory names (kebab-case) — avoid problems on case-insensitive file systems.
- A structure that reflects the business domain at the top levels.
- Tests close to the code they test, or in a test directory with a mirrored structure.
- An
index.ts(barrel) per directory only when the module's public API is stable and explicitly defined.
Anti-patterns
utils/with no criterion: ends up with date formatting files, string helpers, math functions and ID validators all in the same directory.shared/orcommon/with no admission criterion: becomes the newutils/.- Directories with a single file: if there's only one file, the directory probably doesn't add value.
- A structure that copies the framework, not the domain:
controllers/,models/,views/folders as the top level hide what the system does. - Chained barrel re-exports (see below).
Common pitfalls
1. The utils/ folder as a junk drawer
utils/ starts with a date-formatting function and
ends up with 40 files of completely different natures. The
problem isn't having utility code — it's grouping it without a
cohesion criterion. When you can't describe what
utils/ contains in one sentence, it has become a
junk drawer.
Solution: split by responsibility domain.
formatters/ for value formatting,
validators/ for validation, parsers/
for parsing. If a utility semantically belongs to a feature, put
it inside that feature.
2. Barrel re-exports creating circular coupling
Barrel exports (index.ts that re-exports everything
from a directory) are convenient for importing from a module
through a short path. The problem arises when barrels are
chained: A exports B and C;
B imports from A to use C.
Result: a circular dependency that the bundler may resolve in
unpredictable ways.
// anti-pattern: B imports from A to use C (which A re-exports)
// src/users/index.ts
export { UserService } from './UserService';
export { UserUtils } from './UserUtils'; // C
// src/users/UserService.ts
import { UserUtils } from '../users'; // imports A to reach C
// ^^^^^^ creates a circular dependency: UserService -> users/index -> UserService
// correct: import directly
import { UserUtils } from './UserUtils';
Use barrels sparingly. An index.ts is appropriate
for defining a module's public API — what it exposes to other
modules. Don't use barrels as a shortcut to avoid direct imports
within the same module.
3. shared/ directory without an admission criterion
shared/ solves the problem of code used by multiple
features, but without a clear criterion for what belongs there,
it becomes another dumping ground. Two signs that something
doesn't belong in shared/: (1) only one feature
uses it; (2) the code knows about the details of a specific
feature. Neither case is "genuinely shared."
4. Structure that diverges from the actual architecture
A layered structure with controllers/ and
services/ folders is cosmetic if
UserController accesses the database directly. A
feature-based structure is misleading if every feature freely
imports from every other one. The folder structure should
reflect the system's real boundaries — not create them on paper
while the code violates them.
Related topics
Folder structure and feature-based vs layer-based organization are complementary decisions: the organization defines the primary grouping criterion; the folder structure defines the naming conventions, depth and location of each type of artifact within that criterion.
Layered Architecture
defines dependency rules between parts of the system. The folder
structure is how those layers become visible in the file system.
A good structure makes architecture violations obvious — an
import from infra/ into domain/ is
immediately suspicious when directories are named consistently
with the architecture.