Organization

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.ts beside User.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/ or common/ with no admission criterion: becomes the new utils/.
  • 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.