Step 3
Folders as contracts
25 min
Folders as contracts
Freeze "files dropped here automatically do X" as a contract.
1. URL = folder (Next.js App Router)
src/app/
├── posts/
│ ├── page.tsx → /posts
│ └── [id]/
│ └── page.tsx → /posts/:id
Path → URL and back, with grep.
2. No hyphenated composites in path
Express hierarchy via depth, not by admin-pryzeet/.
3. Naming
kebab-case.ts → modules
camelCase.ts → hooks (useX)
PascalCase.tsx → React components
__tests__/*.test.ts → tests
*.spec.ts → Playwright E2E
Pick one set per repo.
4. Barrel exports — stable entry points
src/shared/lib/
├── index.ts ← public API
├── db.ts
├── auth/
│ ├── index.ts
│ ├── session.ts
│ └── oauth.ts
External code imports "@/shared/lib", internals stay refactor-free.
5. Shared vs domains
src/shared/lib/ — domain-agnostic
src/shared/ui/ — visual elements
src/shared/types/ — types only
src/domains/*/ — domain-specific
6. Tests — two schools
- Next to code:
src/lib/foo.ts+src/lib/foo.test.ts - Separate:
src/lib/foo.ts+tests/lib/foo.test.ts
Next-to-code wins for grepability and moves.
7. Docs folder contract (example)
docs/
├── RULES.md — global rules
├── shared/*.md — tech standards
├── agent/{svc}/ — AI instructions
└── service/{svc}/ — product/ops docs
8. _ prefix for private
src/
├── _legacy/ ← forbidden to import
├── _scripts/ ← one-off migrations
Python uses _ by convention; in TS, pair with eslint rules.
9. Gotchas
- Over-nested paths — keep to 3–4 levels
utils/as kitchen sink- Import cycles
- Mixed naming conventions
10. Enforce with ESLint
import boundaries from "eslint-plugin-boundaries";
export default {
plugins: { boundaries },
rules: {
"boundaries/element-types": ["error", {
rules: [
{ from: "frontend", disallow: ["backend"] },
{ from: "shared", disallow: ["domains"] },
],
}],
},
};
Closing
Folder structure is a "makes it findable for others" exercise. A name that depends on your memory fails after three months.
Next
- 04-sql-as-ssot