Every time I have seen a team keep a separate back-end formatting standard, the reason turned out to be historical rather than technical. Same language, same parser, same formatter. A quote style that changes when you cross from app/ to server/ is not a standard, it is an accident that someone documented.
So the short version of this post: use the front-end config on your Node services. What follows is the small set of things that genuinely differ, which is worth writing down precisely because the list is short enough to remember.
The config
{
"singleQuote": true,
"printWidth": 100,
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
"<BUILTIN_MODULES>",
"",
"<THIRD_PARTY_MODULES>",
"",
"^@/config/(.*)$",
"^@/db/(.*)$",
"^@/services/(.*)$",
"^@/routes/(.*)$",
"^@/lib/(.*)$",
"^@/utils/(.*)$",
"",
"^@/types/(.*)$",
"",
"^[./]"
],
"importOrderParserPlugins": ["typescript", "decorators-legacy"],
"importOrderTypeScriptVersion": "5.0.0"
}
Compared to the front-end file: prettier-plugin-tailwindcss is gone, singleAttributePerLine is gone, jsx is out of the parser plugins, and the import groups follow a service layout instead of a component layout. singleQuote and printWidth are identical, and everything unlisted is a Prettier default. That is the entire diff.
I keep decorators-legacy because NestJS and TypeORM code is full of decorators and the Babel parser used by the sort-imports plugin needs to be told. Drop it if you have no decorators; an unnecessary parser plugin costs nothing but a reader's attention.
Built-in modules deserve their own group
This is the one import-ordering difference that is actually about the back end. Front-end files rarely import fs. Service files import node:crypto, node:path and node:fs/promises constantly, and those are the imports you want at the very top, separated, because they tell you the file touches the filesystem or the process before you read a line of it.
<BUILTIN_MODULES> handles this in @ianvs/prettier-plugin-sort-imports, and it matches both fs and node:fs. There is no importOrderBuiltinModulesToTop option to set, in either this plugin or the Trivago one it forked from, despite that name appearing in a lot of example configs including an earlier version of this post. If you paste it in, nothing happens and nothing complains.
While you are there: prefer the node: prefix in the source itself. It is unambiguous about what you are importing, and it makes the group match obvious to a human reading the list, not just to the sorter.
One config for the whole monorepo
In a repo with a web app, a service and a shared types package, I keep a single .prettierrc at the root and use overrides for the parts that differ. Roughly the shape I use in one of my monorepos:
{
"singleQuote": true,
"printWidth": 100,
"plugins": [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
],
"overrides": [
{
"files": ["api/**/*", "workers/**/*"],
"options": { "plugins": ["@ianvs/prettier-plugin-sort-imports"] }
}
]
}
The override exists for one reason: the Tailwind plugin has nothing to sort in a service, and loading it there means every developer needs Tailwind resolvable from that package for the formatter to start. Stripping it from the back-end globs removes a dependency, not a feature.
Note that the plugins array in an override replaces the outer one rather than subtracting from it, so you list what you want rather than what you do not. I have watched this quietly break once, in a monorepo where a shared package formatted differently depending on which app's script had invoked Prettier. One root config, explicit overrides, no per-package .prettierrc files.
Print width is the only option worth arguing about here
Back-end code has a genuine pull toward longer lines: query builders, validation chains, structured log calls with six fields. The old version of this post claimed a wider width "suits validation chains," and that is half true in a misleading way. Prettier will not put a long chain on one line just because the line fits. It breaks method chains and long argument lists by structure, not only by width.
What width actually changes is the threshold at which a borderline expression flips between one line and several. Every flip is a diff, so a width that sits right at the middle of your typical line length maximizes churn. I use 100 everywhere, front end and back end, because the value of one number across the whole repo beats a locally optimal pair of numbers. If you want 120 on a service, the cost is not readability, it is that you now have two numbers and a rule for which applies where.
printWidth is a wrapping preference, not a ceiling. Prettier will exceed it for things it cannot break, like a long string literal or a URL in a comment, which is the same best-effort behavior ruff format has on the Python side.
Wiring
Commit hook. The same Husky plus lint-staged arrangement, with a type check added, because a service is where a broken build is most expensive:
{
"lint-staged": {
"*.{js,ts}": "eslint --cache --fix",
"*.{js,ts,json,yml,yaml,md}": "prettier --write"
}
}
In a monorepo, run each package's own binary so it picks up that package's config, for example pnpm -C api exec eslint --fix. Hoisted tool resolution works right up until two packages want different ESLint versions.
CI. prettier --check ., eslint ., tsc --noEmit. The formatter check is the fastest of the three and it should run first so a formatting failure does not wait behind a type check.
Ignores. This is where back-end repos differ most in practice. Services accumulate generated code: Prisma clients, OpenAPI or gRPC output, database migrations, build artifacts. Generated files should not be formatted, because the generator will win the next time it runs and you will get a diff you did not write. My .prettierignore on a service looks more like:
build
dist
coverage
generated
prisma/migrations
*.generated.ts
Prettier already skips node_modules, version control directories, and paths in a .gitignore found in the directory it runs from, so the file only needs to cover committed output.
Pitfalls
- Formatting migrations. SQL and migration files are a historical record. Reformatting one changes a file that has already run against production databases, which is confusing at best. Ignore the directory.
- Reformatting a vendored or forked file. If you are carrying a patched copy of someone else's module, formatting it turns your next upstream diff into noise.
// prettier-ignoredoes not scale here; a.prettierignoreentry does. - Long log lines that you want on one line. Prettier will break a call with many arguments. If a structured log call reads worse wrapped, the fix is to build the object separately, not to raise
printWidthfor the whole repo. - Expecting the formatter to catch bugs. It formats a floating promise as neatly as an awaited one. Unhandled rejections, missing
await, and unsafeanyare ESLint and TypeScript work, and on the back end they are the failures that page you at night.
For the option-by-option reasoning and the import-plugin history, see the front-end guide. For Python services, the toolchain is entirely different and much simpler: Python formatting. The enforcement model behind all three is in the hub post.