There are many tools for ensuring a constant level of code quality is maintained on a project. Different tools do different things in different ways. This handbook covers some of those tools.
If you have already read this section of the Handbook and are here just for the config files, jump ahead (+ editor files).
Git hooks
Git hooks allow us to run scripts during various git commands. This verification step can be used to run various tools which ensure that the code satisfies some conditions before it is added to the repository. If the verification fails, the git command will abort.
No matter which code quality tools we use, git hooks are a great way to run those tools. There are multiple ways to add git hooks. For JavaScript projects, we recommend using husky.
Follow usage guidelines for installing Husky and add hooks.
After running all commands described in usage chapter, you should have .husky folder with hooks folder inside. For example, if you created pre-commit hook which runs pnpm test command, you should have pre-commit file in .husky folder, with following content
pnpm test
In this example, if you try pushing and the tests fail, code will not get pushed to the remote. We do not necessarily recommend running tests on push, it is just an example (there are better ways to run automated tests using a proper CI/CD set-up).
Most of our code quality tools are run on either pre-commit or pre-push hooks, so using git hooks is kind of a prerequisite for the rest of the Code quality handbook section.
Note: By design husky install must be run in the same directory as .git. You can change directory in your prepare script. Also, you will need to change directory in your hooks. For example, if you have frontend directory where you want to run pre-commit hook, your hook file might look like following
cd frontend && pnpm lint-staged
For more use cases please check Husky documentation.
Lint-staged
Lint-staged works hand-in-hand with commit hooks - pre-commit hook in particular. It allows us to run scripts only on those files which were staged for committing. This makes hooks run faster since they only need to run on a subset of project files instead of all of them. The assumption is that code quality tools have to be run only on modified code while the code that was untouched should already have been checked.
Lint-staged can be configured in many ways. We prefer configuration in .lintstagedrc file in JSON format.
Lint-staged uses glob patterns which allow you to run different scripts on different file types/patterns.
Here is an example which runs prettier and eslint on all staged .js and .ts files, and prettier and stylelint on all staged .scss files via a pre-commit hook:
// .lintstagedrc
{
"**/*.{js,ts}": [
"prettier --write"
"eslint"
]
"**/*.scss": [
"prettier --write",
"stylelint --customSyntax=scss"
]
}
pnpm lint-staged
What lint-staged does is it matches files to glob patterns and passes the list of files as an argument to scripts. Tooling developers should ensure that their scripts can receive the list of files in the correct format. Most common tools like eslint, tslint, stylelint and others are compatible with the way lint-staged passes the list of files.
It is important to note that lint-staged matches files to glob patterns in parallel and runs tasks on those matched groups in parallel as well. Within one matched group, the commands are executed in-order. There is an option to force lint-staged to process groups sequentially if you need to. In the above example .scss files will be processed in parallel with .js and .ts files. For .js and .ts files eslint will be executed first and only after it is done will prettier be executed.
Check the following chapters for specifics about Prettier, ESLint and Stylelint.
Automatic code formatting with Prettier and IDE settings

Source: XKCD
Many people are very passionate about the way they format their code. While we appreciate everyone's opinion, we believe it is best to leave this bikeshedding to automated tooling. It might not format the code in a way that is satisfying to everyone, but it will be consistent across projects and, more importantly, it will format the code written by different people in the same way. This also eliminates discussions around formatting.
Prettier is one of the most popular tools for this job. It is very opinionated and not very configurable. It might not be perfect, but it is a good way to ensure consistency when it comes to formatting and it format multiple different file types.
Here is our recommended Prettier configuration:
// .prettierrc.json
{
"$schema": "http://json.schemastore.org/prettierrc",
"printWidth": 120,
"endOfLine": "lf",
"useTabs": true,
"arrowParens": "always",
"quoteProps": "as-needed",
"bracketSpacing": true,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
}
If you are using Angular, make sure to add an override for templates parser:
// addendum to the above .prettierrc.json
{
...
"overrides": [{
"files": "*.component.html",
"options": {
"parser": "angular"
}
}]
}
If you just added Prettier to an existing codebase, you should probably run it once and let it format the whole codebase. This will probably create a lot of modifications which you can skim through and if it checks out you can commit the changes.
It is possible that some things might break after Prettier is run. In particular, we noticed some issues with the way Prettier formats SCSS, so you might want to exclude SCSS from Prettier in case you notice issues:
# .prettierignore
# It sometimes breaks SCSS (https://github.com/prettier/prettier/issues/6092)
*.scss
If you do not notice issues with Prettier and SCSS, we recommend keeping Prettier on for SCSS files as well.
Developers should set up their code editors to run Prettier whenever they save a file. This is not a bullet-proof solution because some editors might not have support for this (either natively or via plug-ins). Going one step further, we recommend running Prettier via the pre-commit hook. This ensures that the committed code is formatted even if the developer who wrote it did not have his editor set up to format on file save.
npx husky add .husky/pre-commit "prettier --write"
Should generate
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
prettier --write
To ensure editor settings are in-line with prettier settings, create a workspace .vscode settings and .editorconfig files:
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.insertSpaces": false,
"editor.detectIndentation": true
"[scss]": {
"editor.formatOnSave": false
}
}
# .editorconfig
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2 # GitHub uses this value for indentation size when showing code on the Web
insert_final_newline = true
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_style = space
ESLint and TSLint
Even though TSLint is deprecated, it is still used on some projects for legacy reasons. The main reason being that some custom rules packages have not yet migrated to ESLint. If your project does not use custom TSLint rules or you do not use TS, use ESLint.
ESLint configuration
To maintain high code quality and consistency across JavaScript applications, follow a strict linting strategy. This configuration combines industry standards for JavaScript, TypeScript, and Framework-specific best practices.
Angular projects
When initializing or updating a project, extend the following recommended configurations to ensure a robust baseline:
| Config | Description |
|---|---|
angular.configs.tsRecommended |
Official Angular linting for TypeScript. |
angular.configs.templateAll |
Strict rules for Angular HTML templates. |
eslint.configs.recommended |
Core JavaScript rules. |
eslintConfigPrettier |
Disables linting rules that might conflict with Prettier. |
tseslint.configs.recommended |
TypeScript-specific best practices and formatting. |
tseslint.configs.stylistic |
TypeScript-specific best practices and formatting. |
Project-Specific Rules
While the recommended sets cover the basics, we enforce additional rules to prevent common pitfalls in Angular (such as breaking Server-Side Rendering) and to keep the git history clean.
Add these to your overrides section:
{
"rules": {
"no-console": ["error", { "allow": ["warn", "error"] }],
"no-restricted-globals": [
"error",
{
"name": "window",
"message": "Use InjectionToken instead of direct window manipulation."
},
{
"name": "document",
"message": "Use InjectionToken instead of direct document manipulation."
},
{
"name": "fdescribe",
"message": "Do not commit fdescribe. Use describe instead."
}
{
"name": "fit",
"message": "Do not commit fit. Use it instead."
}
]
}
}
React projects
For React and Next.js projects, use local ESLint flat configs (eslint.config.mjs) instead of shared legacy plugin presets.
Why:
js-linters(@infinum/eslint-plugin) is deprecated for React/Next.js use cases.- Maintaining one shared package across Angular, React, Next.js and different project constraints created too much maintenance overhead and upgrade friction.
- Local config composition is easier to evolve per project and lowers breaking-change risk.
Reference starter: infinum/JS-React-Example.
JS-React-Example keeps reusable ESLint configs in packages/configs; each use case has its own module (base, react, nextjs, typescript, etc.), and projects build their lint setup by composing those modules.
Recommended configuration stack (React + Next.js)
Use these baseline config sets in React/Next.js apps:
| Config / Preset | Description |
|---|---|
pluginJs.configs.recommended |
Core JavaScript correctness rules from @eslint/js. |
tseslint.configs.stylisticTypeChecked |
Type-aware TypeScript rules and stylistic consistency. |
pluginReact.configs['jsx-runtime'] |
React JSX runtime rules (no legacy React import requirement). |
pluginReactHooks.configs.recommended |
Hooks safety (rules-of-hooks and dependency checks). |
pluginNext.configs.recommended |
Next.js framework rules. |
pluginNext.configs['core-web-vitals'] |
Additional Next.js performance and quality rules. |
eslint-plugin-prettier/recommended |
Prevents style-rule conflicts and integrates Prettier. |
Required dev dependencies for flat config:
pnpm add -D eslint@^9 @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks @next/eslint-plugin-next eslint-plugin-prettier eslint-config-prettier globals
If eslint-plugin-react-hooks still expects older rule APIs in your setup, wrap it with fixupPluginRules from @eslint/compat (as done in JS-React-Example).
Minimal composition example:
import baseConfig from "@infinum/configs/eslint/base";
import typescriptConfig from "@infinum/configs/eslint/typescript";
import reactConfig from "@infinum/configs/eslint/react";
import nextConfig from "@infinum/configs/eslint/nextjs";
import jestConfig from "@infinum/configs/eslint/jest";
export default [
...baseConfig,
...typescriptConfig,
...reactConfig,
...nextConfig,
...jestConfig,
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parserOptions: {
project: ["./tsconfig.eslint.json"],
tsconfigRootDir: import.meta.dirname,
},
},
},
];
Project-specific React/Next rules
On top of recommended presets, keep these extra rules where applicable:
{
files: ['**/*.{ts,tsx,js,jsx}'],
rules: {
'react/no-unknown-property': ['error', { ignore: ['css'] }],
'react/self-closing-comp': ['warn', { component: true, html: true }],
'react/prop-types': ['error', { skipUndeclared: true }],
'react-hooks/exhaustive-deps': ['error', { additionalHooks: '(useSafeLayoutEffect|useUpdateEffect)' }],
},
}
Custom Next.js rule: no hooks in pages/ folder
If a project still uses the Pages Router (pages/ or src/pages/), keep this custom rule.
For pure App Router projects (app/ only), this rule is usually not needed.
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator(() => "");
export default createRule({
name: "no-hooks-in-pages-folder",
meta: {
type: "problem",
docs: { description: "Disallow React hooks in `pages` folder" },
schema: [],
messages: {
noHooksInPagesFolder:
"React hook '{{hookName}}' not allowed in {{filename}}",
},
},
defaultOptions: [],
create(context) {
const forbiddenFolderRegex = /((\/|^)src\/pages\/|(\/|^)pages\/)/;
const isReactHook = (node: TSESTree.CallExpression) =>
node.callee.type === "Identifier" && node.callee.name.startsWith("use");
return {
CallExpression(node) {
const filename = context.filename;
if (!forbiddenFolderRegex.test(filename)) return;
if (!isReactHook(node)) return;
context.report({
node,
messageId: "noHooksInPagesFolder",
data: {
hookName: (node.callee as TSESTree.Identifier).name,
filename,
},
});
},
};
},
});
Register it in flat config as a local plugin:
import noHooksInPagesFolder from "./src/rules/no-hooks-in-pages-folder";
export default [
// ...other config arrays
{
files: ["**/*.{ts,tsx,js,jsx}"],
plugins: {
local: {
rules: {
"no-hooks-in-pages-folder": noHooksInPagesFolder,
},
},
},
rules: {
"local/no-hooks-in-pages-folder": "error",
},
},
];
Migration from js-linters
If an existing project still uses legacy extends with @infinum/eslint-plugin, migrate it to flat config composition.
Legacy (.eslintrc):
{
"extends": [
"plugin:@infinum/core",
"plugin:@infinum/typescript",
"plugin:@infinum/react",
"plugin:@infinum/next-js",
"plugin:@infinum/chakra-ui"
]
}
Target (eslint.config.mjs):
import baseConfig from "@infinum/configs/eslint/base";
import typescriptConfig from "@infinum/configs/eslint/typescript";
import reactConfig from "@infinum/configs/eslint/react";
import nextConfig from "@infinum/configs/eslint/nextjs";
export default [
...baseConfig,
...typescriptConfig,
...reactConfig,
...nextConfig,
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parserOptions: {
project: ["./tsconfig.eslint.json"],
tsconfigRootDir: import.meta.dirname,
},
},
},
];
Migration checklist:
- Move from
.eslintrc*toeslint.config.mjs(flat config). - Upgrade
eslintto v9 before enabling flat config composition. - Install flat-config dependencies (
@eslint/js,typescript-eslint,eslint-plugin-react,eslint-plugin-react-hooks,@next/eslint-plugin-next). - Replace plugin preset strings with imported config arrays.
- Keep TypeScript parser
projectconfig for typed rules (tsconfig.eslint.json). - Re-add any project-specific overrides explicitly (they are no longer inherited implicitly).
- Add local custom rules (like
no-hooks-in-pages-folder) only where they match the routing architecture.
Automation & License Headers
To ensure compliance across the team, we automate the maintenance of license headers and code style before any code is committed.
Rule: Use the
header/headerESLint rule to define the required license format.Workflow: Combine this with Husky and lint-staged to automatically check or inject headers during the pre-commit phase.
Note: This automation ensures that no file is pushed to the repository without the proper legal boilerplate, reducing manual overhead during code reviews.
Stylelint
If you have no issues with prettier SCSS formatting and you decide to use Prettier, it is recommended to install stylelint-prettier plugin and preset.
{
"plugins": ["stylelint-prettier"],
"rules": {
"prettier/prettier": true
}
}
If you want to use stylelint, it is recommended to use @infinumjs/stylelint-config. In order to use it with prettier, please add stylelint-config-prettier.
{
"extends": [
"@infinumjs/stylelint-config",
"stylelint-config-prettier" // needed if you use prettier to format SCSS
]
}
Putting it all together
Here is the complete example which runs TypeScript compilation check on all files, prettier, eslint and stylelint on an Angular (v10) project:
{
"scripts": {
"prepare": "husky install",
"tsc": "concurrently \"pnpm tsc:app\" \"pnpm tsc:spec\"",
"tsc:app": "tsc --noEmit -p ./src/tsconfig.app.json",
"tsc:spec": "tsc --noEmit -p ./src/tsconfig.spec.json"
}
}
// .lintstagedrc.json
{
"**/*.{json,md,html}": [
"prettier --write"
],
"**/*.{js,ts}": [
"prettier --write",
"eslint"
],
"**/*.css": "stylelint",
"**/*.scss": [
"prettier --write" // add or remove this line depending on whether you run stylelint on SCSS,
"stylelint --customSyntax=post-scss"
]
}
pnpm tsc && pnpm lint-staged --config .lintstagedrc.json
// .stylelintrc.json
{
"plugins": ["stylelint-prettier"],
"rules": {
"prettier/prettier": true
},
"extends": [
"@infinumjs/stylelint-config",
"stylelint-config-prettier"
],
"overrides": [
{
"files": ["*.scss", "**/*.scss"],
"customSyntax": "postcss-scss"
}
]
}
For this example, you will have to install the following devDependencies:
pnpm i -D -E concurrently husky lint-staged stylelint stylelint-prettier prettier tslint-config-prettier
Some notes:
- it is important to run
tscon all files because changes in staged files can affect compilation of unmodified files tscis run on both the applicationtsconfigfiles and teststsconfigfilesconcurrentlyspeeds up things by running tsc checks in parallelprettier --writeis run separately for.tsand other files in order to prevent any possible race conditions before running TSLint (vialint:ng) and Prettier