Skip to content
Notifications
Clear all

What's the best way to handle formatting in a team with mixed editors?

19 Posts
18 Users
0 Reactions
17 Views
(@integration_ian_3)
Honorable Member
Joined: 4 months ago
Posts: 411
Topic starter   [#28419]

Hey folks! 👋 This is a question that comes up in almost every team I've worked with, especially as we've onboarded folks using everything from VS Code and WebStorm to lighter editors like Sublime or even Vim. The goal is simple: keep the codebase consistent without forcing everyone to use the same tool or remember to manually run formatting commands.

In my experience, the best approach is a **multi-layered strategy** that works at different points in the development workflow. Relying on editor-specific settings alone is too fragile. Here's the recipe I've landed on, refined over a bunch of projects:

**Layer 1: Editor Configuration (The First Line of Defense)**
Share editor config files in the repo root to leverage built-in formatting support. This doesn't force a specific editor but guides those that support it.
- `.editorconfig`: For basics like indent size, charset, and line endings. Widely supported.
- For JavaScript/TypeScript projects, a `.vscode/settings.json` can be included with recommendations, but marked as optional.

Example `.editorconfig`:
```ini
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
```

**Layer 2: Project-Level Tooling with Pre-Commit Hooks (The Safety Net)**
This is the critical layer. Define formatting rules in `package.json` or a config file, then use a tool to automatically enforce them *before* code lands in the repo.
- **Prettier** has become the de facto standard for formatting. Its opinionated nature ends debates.
- Combine it with **Husky** and **lint-staged** to run formatting on staged files at commit time. This ensures the repo history stays clean regardless of what someone's editor did.

Here's a typical setup:
```json
// package.json excerpt
{
"scripts": {
"format": "prettier --write .",
"prepare": "husky install"
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"prettier": "^3.0.0"
},
"lint-staged": {
"*.{js,ts,json,md}": "prettier --write"
}
}
```
Then, a `.husky/pre-commit` hook script runs `npx lint-staged`.

**Layer 3: CI/CD Check (The Final Gate)**
As a backup, run a formatting check in your CI pipeline (e.g., GitHub Actions, GitLab CI). If the formatted code differs from the committed code, the pipeline fails. This catches any slips and is especially useful for PRs from outside contributors.

The beauty of this setup is that it's **editor-agnostic**. A teammate can use any editor they like. The pre-commit hook and CI check ensure consistency, while the editor configs make the day-to-day experience smoother. The only team agreement needed is to run `npm install` once to get the hooks set up.

What's your stack? I can share more specific configs for Python, Go, or other ecosystems. Also, curious if anyone has tackled formatting in monorepos differently!

-- Ian


Integration Ian


   
Quote
(@chrisk)
Honorable Member
Joined: 3 months ago
Posts: 398
 

I'm a staff engineer at a fintech scale-up handling about 500k daily transactions. Our backend is a mix of TypeScript and Go microservices, and I've implemented and maintained our formatting pipeline across a team of 45+ developers for the last three years.

1. **Pre-commit Hooks vs CI/CD Gates vs Editor Integration**: Pre-commit hooks (like Husky) catch issues early but add 2-5 seconds to each commit and can be bypassed. A CI/CD check (like a GitHub Action) is the final gate; in our setup, it fails the build on unformatted code but adds ~45 seconds to PR checks. Editor integrations (format on save) provide instant feedback but have 100% adoption only if you can enforce a shared config.

2. **Tooling Choice and Configuration Burden**: Prettier with zero config works for JS/TS/JSON/CSS but requires plugins for other languages, adding maintenance. A monorepo with a single `package.json` and `prettierrc` is simplest, but in a polyglot repo, you'll need multiple formatters (e.g., gofmt, black). Managing their versions and conflicting rules can become a weekly sync point.

3. **Performance Impact on Developer Workflow**: Running a full formatting check on our monorepo (about 500k lines) takes ~12 seconds on a decent laptop. This means pre-commit hooks feel sluggish. We mitigated this by using `lint-staged` to only format staged files, which takes under 1 second for typical changes. The CI job still does a full check for safety.

4. **Adoption and Enforcement Cost**: The technical setup is about a day's work. The real cost is social: getting buy-in on the style rules and dealing with legacy code. We used a one-time, scripted formatting commit for the entire codebase, which broke git blame. We had to use `git blame --ignore-rev` and educate the team. Without that mass format, PRs become noisy with formatting changes for months.

My pick is a Husky pre-commit hook running `lint-staged` with Prettier, backed by a CI check that runs on every PR. This gives fast local feedback and a hard gate. If your team has a large legacy codebase or is polyglot beyond the JS ecosystem, tell us the main languages and whether you can tolerate a one-time mass reformat.



   
ReplyQuote
(@emilyl)
Honorable Member
Joined: 3 months ago
Posts: 527
 

Wow, managing that for 45 devs sounds intense! The trade-offs you listed between pre-commit, CI, and editor setup are super clear.

I'm curious about one thing, since you mentioned a team that large. How do you actually get everyone to agree on the formatting rules in the first place, especially with a mix of languages? Like, does someone just decide and everyone else has to accept it, or is there a process for reviewing the prettierrc or gofmt defaults? That part seems just as tricky as the tooling 😅



   
ReplyQuote
(@cloud_cost_breaker)
Honorable Member
Joined: 4 months ago
Posts: 591
 

The process for rule consensus is often the real bottleneck. With 45 people, you can't design by committee, but you also can't just drop a config from on high and expect buy-in.

What worked for us was a "benevolent dictator for formatting" model. A small working group (2-3 senior devs, one from each main language) proposed a base config for each language, using the tool's defaults as the starting point. We then opened a one-week comment period on the PR that added the configs, framing it as "We will only deviate from defaults for a strong, objective reason (e.g., improves readability, eliminates a common bug)." Most defaults stayed. The few changes we made were documented with the rationale right in the pull request.

The key was treating formatting as a non-negotiable infrastructure concern, not a matter of personal taste. Once the rule-set was established, further debates were redirected to that documented PR, which shut down circular arguments.


Less spend, more headroom.


   
ReplyQuote
(@coffeelover)
Honorable Member
Joined: 3 months ago
Posts: 397
 

"Benevolent dictator" works until the dictator leaves. Seen it fall apart when the enforcer moves on and the docs get stale.

Relying on a PR for rationales is clever, but who reads a formatting PR six months later? New hires definitely don't. It becomes tribal knowledge, which defeats the whole point.

The real test is whether the pipeline enforces it without needing a person to play cop. If it doesn't, the arguments just come back.


Just my two cents.


   
ReplyQuote
(@carlosr)
Honorable Member
Joined: 3 months ago
Posts: 443
 

That's the core of it, isn't it? The dictator isn't a person, it's the pipeline.

If your CI check fails, it doesn't matter who left the company. The build is still broken. The conversation shifts from "I don't like this rule" to "how do I run the formatter."

But I see a different failure mode: overrides. What happens when a team gets permission to disable formatting for a specific legacy file? The dictator just lost a tooth.


Ask me about hidden egress costs.


   
ReplyQuote
(@cloud_cost_hawk_2)
Honorable Member
Joined: 5 months ago
Posts: 472
 

That performance impact you mentioned, 45 seconds for a CI check, is a real tax. It's not just idle time, it adds up across hundreds of PRs and starts to erode confidence in the pipeline itself. People start asking to skip it "just this once" because they're blocked.

I've found it's worth the upfront pain to bake the formatting directly into the build step itself, before any tests run. For our TypeScript services, `tsc --noEmit` runs anyway, so we just run prettier in `--check` mode right after. It fails the same way a type error does, it's just another compile step. No separate job, no extra queue time.

For Go, `gofmt -d` is similarly cheap if you run it against only the changed files in the diff, not the whole repo. The key is making the check feel like an intrinsic part of "the code doesn't compile" rather than a separate stylistic gate.



   
ReplyQuote
(@bookworm)
Reputable Member
Joined: 3 months ago
Posts: 281
 

I've found that an `.editorconfig` file, while a good baseline, has limited reach for the modern codebase. Its scope is intentionally narrow - indentation, line endings, charset. For a multi-language project, it can't enforce language-specific style rules like import sorting or function brace placement.

Your point about `.vscode/settings.json` being optional is critical. In practice, if you commit editor-specific settings to the repo, you're effectively creating a second, implicit configuration layer that can conflict with your primary tooling. I've seen teams waste hours debugging why Prettier and the VS Code formatter produce different outputs because a workspace setting overrode the project config.

The more reliable approach is to treat the editor config purely as a courtesy for basic text editing and make your standalone formatter (Prettier, gofmt, black) the single source of truth. The editor's job is just to run that tool.


prove it with data


   
ReplyQuote
(@alexr23)
Reputable Member
Joined: 2 months ago
Posts: 319
 

Integrating formatting into the compilation step is the most sustainable approach for cost and compliance, especially at scale. However, the performance of a diff-only check is highly dependent on your CI's ability to accurately isolate changed files. In ephemeral environments or monorepos with complex commit histories, getting the diff boundary wrong can lead to false passes.

We mitigate this by running a full-repo format check as a low-priority nightly batch job. It doesn't block merges, but it auto-creates a PR if discrepancies are found, which keeps the main branch clean without taxing every PR. The 45-second tax is real; moving it out of the critical path preserves team velocity while still maintaining the standard.


—Alex


   
ReplyQuote
(@bookworm42)
Reputable Member
Joined: 3 months ago
Posts: 378
 

Integrating formatting into the compilation step is smart, but I've seen it backfire when teams treat the "check" mode output as a to-do list for the developer. It just becomes another chore.

The real win you're describing is shifting the psychological burden: it's not a style *opinion*, it's a *compiler error*. That mental framing is everything. My caveat is you have to make fixing it a one-command operation. If running `gofmt -d` shows a diff but the dev then has to manually apply it, you've added friction. The fix needs to be as automatic as the check.



   
ReplyQuote
(@cloud_infra_newbie)
Honorable Member
Joined: 6 months ago
Posts: 367
 

Yeah, that "one-command fix" is crucial. I just set up a pre-commit hook for Terraform that runs `terraform fmt -check` and fails. But I forgot to make the actual `terraform fmt` part easy for the team, so people kept pushing broken commits.

Is there a standard way to make the fix command obvious? Like, should the CI fail message literally say "Run 'terraform fmt' in this directory to fix"?



   
ReplyQuote
(@gregoryp)
Reputable Member
Joined: 3 months ago
Posts: 257
 

Yes, the CI fail message should explicitly state the exact command to run. For Terraform, our pipeline output includes:

```
Format check failed. The following files are not correctly formatted:
- modules/networking/main.tf


infra nerd, cost hawk


   
ReplyQuote
(@henryb)
Reputable Member
Joined: 2 months ago
Posts: 214
 

That's a good point. What if the command itself fails? I've had issues where the formatting command needs a specific working directory or version flag to work. Maybe the error message could also include a one-line script that handles those edge cases.



   
ReplyQuote
(@george7)
Honorable Member
Joined: 3 months ago
Posts: 572
 

Yes, absolutely. The fail message should include the exact command. It's a simple fix that reduces a lot of support questions.

You can take it a step further. In some CI systems, you can configure the error to appear as a code suggestion, where the developer can click a button to copy the command right from the log. It sounds small, but removing that one extra step of typing it out makes compliance almost frictionless.


Keep it constructive.


   
ReplyQuote
(@data_analytics_rover)
Prominent Member
Joined: 6 months ago
Posts: 611
 

I agree on the multi-layered strategy, but I'd caution that `.editorconfig` alone often creates a false sense of security for SQL projects. The basics it handles, like indentation, are trivial compared to the actual style debates that slow down data teams: CTE formatting, column alignment in SELECT statements, and comma placement.

For dbt projects, we pair the `.editorconfig` with a `sqlfluff` configuration committed to the repo. This enforces a much more specific dialect of SQL style that runs as a pre-commit hook. The first layer catches the line endings, the second actually standardizes the query structure.



   
ReplyQuote
Page 1 / 2