Overview

Project layout is the first thing a new contributor sees and the first thing an agent has to model. A predictable shape per language pays back every search, every import, every refactor. The rules below cover the canonical layouts for Python, Node, Rust, and Go; where tests belong; when src/ is worth the extra path segment; and the rule that one project equals one repo until proven otherwise. See folder-hierarchy for what goes inside.

One project equals one repo

Default to one repo per shippable artifact. A repo is a versioning boundary, a CI scope, and a CODEOWNERS surface; sharing it across unrelated projects scrambles all three.

Break the rule only when:

  • Two projects share a library that changes in lockstep with both. Move to a monorepo with pnpm or Turborepo before the copy-paste rot starts.
  • A binary and its tightly coupled tooling ship together (a Rust CLI plus its xtask).

A “let’s add it here for now” service in someone else’s repo is technical debt by Tuesday. Spin a new repo.

Use src/ when the language expects it

src/ adds a path segment. It earns its keep when the language or toolchain treats it as the package boundary; otherwise it is noise.

  • Python: use src/<package>/ for any installable library. It forces pytest to import the installed package, not the working copy, which catches missing __init__.py and missing package_data before release.
  • Rust: Cargo already uses src/. Do not fight it.
  • Go: skip src/. The Go toolchain expects cmd/ and internal/ at repo root.
  • Node libraries: use src/ so the build output (dist/) does not collide with sources.
  • Node apps with no build step: skip src/. The extra segment buys nothing.

Canonical layouts

The shape per ecosystem:

# Python library
my_project/
├── pyproject.toml
├── README.md
├── src/my_project/
│   ├── __init__.py
│   └── core.py
└── tests/
    └── test_core.py

# Node monorepo workspace
my-pkg/
├── package.json
├── src/
│   └── index.ts
├── tests/
└── dist/        # build output, gitignored

# Rust binary
my-tool/
├── Cargo.toml
├── src/
│   ├── main.rs
│   └── lib.rs
└── tests/

# Go service
my-svc/
├── go.mod
├── cmd/my-svc/main.go
├── internal/
│   └── handler/
└── pkg/         # only if shared across repos

The directory names are not optional. Cargo, Go modules, and most Python tooling parse them.

Tests next to source vs. apart

Two patterns work; pick one and stay with it.

  • tests/ at repo root, mirroring src/ (Python convention, Rust integration tests). Reviewers see “did the test change?” without scrolling past code.
  • Tests next to source (Foo.tsx plus Foo.test.tsx, Go _test.go). Rename and delete refactors stay atomic.

Avoid mixing. A repo that puts unit tests next to source and integration tests in tests/ is fine; one that drops half the unit tests in each location is a search-and-replace bug waiting to happen. See testing.

Top-level files belong at the root

The root directory is signage. Keep it short, keep it scannable.

  • README.md, LICENSE, CHANGELOG.md, and the canonical build manifest (pyproject.toml, package.json, Cargo.toml, go.mod).
  • One .gitignore, one .editorconfig, and the CI config (.github/workflows/).
  • Nothing else. Move scripts to scripts/, docs to docs/, examples to examples/.

A repo root with 30 files at the top hides the README. The README is what new readers click first.

Mirror the layout in tests

The test tree mirrors the source tree path-for-path. src/parser/invoice.py gets tests/parser/test_invoice.py. A test file with no corresponding source file is a hint that the source moved without its test.

Tooling depends on this: pytest’s collection, Jest’s --findRelatedTests, and Go’s per-package test runner all assume parallel trees.

Build output is gitignored, always

dist/, build/, target/, out/, __pycache__/, .next/: every one of these goes in .gitignore from the first commit. Committing build output bloats the repo, confuses reviewers, and breaks git diff. The exception is a generated file that is genuinely the source of record (e.g., a vendored protobuf). Mark those explicitly; do not let them sneak in via inertia.