Overview
Project layout is the first thing a contributor sees and the first thing an agent has to model. A predictable shape per language pays back on every search and import. Keep one project per repo by default, follow the canonical layout per ecosystem, keep the repo root short, and gitignore build output from the first commit. What goes inside the folders is in folder-hierarchy.
Keep one project per repo by default
A repo is a versioning boundary, a CI scope, and a CODEOWNERS surface. Sharing one across unrelated projects blurs all three. Combine projects only when:
- Two projects share a library that changes in lockstep with both. Move to a monorepo before copy-paste rot starts.
- A binary and its tightly coupled tooling ship together, such as a Rust CLI and its
xtask.
Use src/ when the toolchain expects it
- Python libraries: use
src/<package>/. Tests then import the installed package, not the working copy, which exposes a missing__init__.pyor packaged data file before release. - Rust: Cargo already uses
src/. - Go: skip
src/. Go’s module-layout guide puts server logic ininternal/and commands incmd/at the repo root. - Node libraries: use
src/so build output indist/does not mix with sources. - Node apps with no build step: skip it.
Follow the canonical layout per ecosystem
# Python library
my_project/
├── pyproject.toml
├── src/my_project/
│ ├── __init__.py
│ └── core.py
└── tests/test_core.py
# Node package
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/
Cargo, Go modules, and most Python tooling parse these directory names, so they are not optional. A Go pkg/ directory is a community habit, not part of Go’s layout guide; add it only when you deliberately export importable packages.
Pick one test location and mirror the source
tests/at the repo root, mirroringsrc/path for path (Python convention, Rust integration tests).src/parser/invoice.pygetstests/parser/test_invoice.py.- Tests beside the source (
Foo.tsxwithFoo.test.tsx, Go_test.go). Renames and deletions stay atomic.
Unit tests beside source plus integration tests in tests/ is fine. Half the unit tests in each place is not. A test with no matching source file means the source moved without it. See testing and naming-conventions.
Keep the repo root short
The root should hold README.md, LICENSE, CHANGELOG.md, the build manifest (pyproject.toml, package.json, Cargo.toml, go.mod), one .gitignore, one .editorconfig, and CI config (.github/workflows/). Move scripts to scripts/, docs to docs/, and examples to examples/, so the README stays visible.
Gitignore build output from the first commit
dist/, build/, target/, out/, __pycache__/, and .next/ bloat the repo and break git diff if committed. The exception is a generated file that is the source of record, such as a vendored protobuf; mark it explicitly.