---
title: "Project Structure"
slug: "project-structure"
category: "file-organization"
tags: ["file-organization", "project-structure", "python", "node", "rust", "go"]
status: "stable"
last_updated: 2026-05-14
summary: "Layout patterns by language and framework, when to use src/, where tests live, and the one-project-one-repo rule."
related: ["[[file-organization/folder-hierarchy]]", "[[file-organization/monorepo]]", "[[file-organization/naming-conventions]]", "[[coding/general-principles]]", "[[tooling/github]]"]
---

## 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 [[file-organization/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 [[file-organization/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 [[coding/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.

## Related

- [[file-organization/folder-hierarchy]]
- [[file-organization/monorepo]]
- [[file-organization/naming-conventions]]
- [[coding/general-principles]]
- [[tooling/github]]
