code-style-guide
Establish, document, and enforce a code style guide for a codebase - naming, structure, formatting, lints. Use when the user asks to set up a style guide, define coding conventions, configure linters/formatters, or enforce consistency across a team.
A style guide isn't about taste — it's about removing decisions so the team can think about the actual problem. The best style guide is the one you can stop arguing about.
When to use
- "Set up a style guide for this project"
- "What's the convention here?"
- "Configure ESLint / Prettier / Biome / PHP-CS-Fixer / …"
- "Enforce consistent code style"
- "Review this code for style"
Core principle
Automate everything you can. Document only what you can't.
The order of preference:
- Formatter — runs on save, no debate (Prettier, Biome,
gofmt,rustfmt, Black, …) - Linter — catches errors and enforced patterns (ESLint, Biome, Pylint, Rubocop, golangci-lint, PHPStan, …)
- Type checker — TypeScript strict, mypy strict, Phpstan max, …
- Pre-commit hook — make it impossible to commit unformatted code
- CI gate — last line of defense
- Written guide — only for things the tools can't enforce
If you find yourself adding a "rule" to the written guide that a linter could enforce, configure the linter instead.
Procedure
- Detect the stack and existing conventions — read 5–10 representative files before proposing anything
- Pick a base style rather than inventing one (see "Pick a base" below)
- Configure the formatter + linter with that base
- Add the pre-commit hook
- Add the CI gate
- Write the short prose guide for things tools can't catch
- Run the formatter across the codebase once in a dedicated PR — no review on that PR, just code formatting
Pick a base
Don't invent. Adopt one and tweak.
| Language | Strong default base |
|---|---|
| JavaScript/TypeScript | Prettier + ESLint with eslint:recommended + @typescript-eslint/recommended, or Biome (one tool for both) |
| Python | Black + Ruff (Ruff replaces most of flake8/isort/pylint) |
| Go | gofmt is the law; add golangci-lint with staticcheck, govet, errcheck |
| Rust | rustfmt + clippy with -W clippy::pedantic reviewed selectively |
| PHP | Laravel: Pint; otherwise PHP-CS-Fixer with @PSR12 + PHPStan level 8 |
| Ruby | RuboCop with rubocop-rails-omakase (or Standard) |
| Java | Spotless + Checkstyle + Error Prone |
What to put in the written guide
Only things the tools can't enforce. Keep it short — a page, max two.
Worth documenting
- Naming patterns the linter can't validate (e.g., "components are nouns, hooks start with
use, store modules are singular nouns") - File/directory layout ("controllers in
app/Http/Controllers/, one resource per file") - Module boundaries ("the
authpackage depends oncorebut never vice versa") - Conventions for things with multiple valid options (e.g., "prefer async/await over
.then()") - Error handling style ("prefer Result types over throws in the domain layer; throws at boundaries")
- Test layout ("one test file per module, mirror the source tree, name
*.test.ts") - Comment policy ("comments explain why; if the code needs a comment to explain what, refactor")
- Commit message format (see
conventional-commitsskill)
Not worth documenting
- Indent width — formatter
- Quote style — formatter
- Semicolons — formatter
- Trailing commas — formatter
- Import order — formatter or linter
- Line length — formatter
- Anything subjective with no observed cost when violated
Naming guidance
Pick one convention per category and stick with it.
| Element | Common choice |
|---|---|
| Constants | UPPER_SNAKE_CASE |
| Variables, parameters | camelCase (JS/TS/Java/PHP), snake_case (Python/Ruby/Rust) |
| Functions | same as variables |
| Classes, types | PascalCase everywhere |
| Interfaces | PascalCase; no I prefix |
| Files | match the dominant export: UserProfile.tsx for a UserProfile component, kebab-case.ts for utility modules |
| Booleans | is, has, should, can prefix: isOpen, hasPermission |
| Event handlers | handleX (component-internal), onX (exposed as prop) |
| Async functions | no special suffix; the return type is the signal |
What matters more than the choice is consistency within the codebase.
Configuration starting points
TypeScript + Biome (single tool, fast)
// biome.json
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 },
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": { "noUnusedVariables": "error" },
"style": { "useConst": "error", "useTemplate": "error" }
}
},
"organizeImports": { "enabled": true }
}
Python
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF"]
[tool.black]
line-length = 100
target-version = ["py312"]
Pre-commit (universal)
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: format
name: format
entry: <your formatter command>
language: system
pass_filenames: false
- id: lint
name: lint
entry: <your linter command>
language: system
pass_filenames: false
Or use Husky for JS projects, Lefthook for cross-language.
Rolling it out to an existing codebase
The trick: don't try to fix everything in one PR.
- PR 1: add config files (formatter, linter, hooks), do not apply yet
- PR 2: run the formatter on the whole codebase. No other changes. Use
.git-blame-ignore-revssogit blamedoesn't blame this commit:echo "$(git rev-parse HEAD)" >> .git-blame-ignore-revs git config blame.ignoreRevsFile .git-blame-ignore-revs - PR 3: enable lint rules that auto-fix
- PR 4+: tackle remaining lint warnings in small batches, by category, never by file count
Don't --no-verify your way past the new hooks. If a hook is wrong, fix the rule.
Output format
When proposing or reviewing a style setup:
## Style guide: <project>
**Language(s):** …
**Tooling chosen:**
- Formatter: <name + config file>
- Linter: <name + config file>
- Type checker: <name + level>
- Pre-commit: <hook tool>
- CI gate: <workflow file>
**Base style adopted:** <e.g., Biome recommended, PSR-12, PEP 8 via Black>
**Project-specific rules (in written guide):**
- <rule>
**Migration plan (if existing codebase):**
1. …
2. …
Anti-patterns
- ❌ A 40-page style guide when a formatter + 5-rule preamble would do
- ❌ Style rules that aren't enforced — they degrade into folklore
- ❌ Different formatter config in each subdirectory ("legacy uses tabs")
- ❌ Bikeshedding on tab vs spaces when the formatter picks for you
- ❌ Style PRs that also change behavior — keep them separate
- ❌ "We follow Airbnb except…" with 30 exceptions — pick a different base
- ❌ Disabling lint rules inline (
// eslint-disable) without a comment explaining why - ❌ Letting one team member's PR push through with
--no-verify
Tips
- Run the formatter in your editor on save. "Format before commit" is too late and creates churn.
- Enable
editor.formatOnSavein a committed.vscode/settings.jsonso new contributors get it automatically. - Pin tool versions (
.nvmrc,pyproject.toml,composer.json) — different versions produce different output. - Disagreement about a rule is fine; relitigating it every week is not. Decide, write it down, move on.
- The style guide is a living doc, but rare to edit. If it's changing weekly, you're rule-fitting to last week's PR review.
References
- Google Style Guides — opinionated, battle-tested, multi-language
- Airbnb JavaScript Style Guide — popular JS reference
- PEP 8 — Python style
- Effective Go — style + idioms together
- The Tao of Code — Bryan Cantrill on enforced style