Skip to main content

Linter

Refract uses ESLint with the flat config format and Prettier for formatting. Every package has its own config, but they all follow the same conventions. Here’s everything you need to know to stay in sync with the linter — and to make Cursor catch problems before you even save a file.

ESLint configuration

Refract uses ESLint’s flat config format, which means each package has an eslint.config.mjs (or .js) file at its root instead of the older .eslintrc format. Flat config gives you explicit, composable control over which rules apply where.

Backend (apps/backend/eslint.config.mjs)

The backend config extends typescript-eslint’s recommended rules, integrates Prettier (via eslint-config-prettier/flat to turn off any rules that conflict with formatting), and adds import ordering enforcement:
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
import importPlugin from 'eslint-plugin-import';

export default [
  { ignores: ['**/server/dist/**', 'dist/**'] },
  ...tseslint.configs.recommended,
  eslintConfigPrettier,
  {
    files: ['./src/**/*.ts'],
    plugins: { import: importPlugin },
    rules: {
      'import/order': ['warn', {
        groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'type'],
        'newlines-between': 'never',
        alphabetize: { order: 'asc', caseInsensitive: true },
      }],
    },
  },
];
The import/order rule enforces alphabetical, grouped imports — so Node built-ins come first, then external packages, then your own files. This keeps diffs clean and makes it obvious at a glance what a file depends on.

Frontend (apps/frontend/eslint.config.js)

The frontend adds React-specific rules on top of the TypeScript base:
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
    },
  },
);
The react-hooks plugin catches things like missing dependencies in useEffect — the kind of bug that causes subtle stale-closure issues that are painful to debug in production.

Tool packages and shared

Tool packages (apps/tools/*/*) and apps/shared use the same minimal config: typescript-eslint recommended + Prettier + project-aware type checking:
export default [
  { ignores: ['dist/**', 'node_modules/**'] },
  { files: ['./src/**/*.ts'], languageOptions: { parserOptions: { project: './tsconfig.json' } } },
  ...tseslint.configs.recommended,
  eslintConfigPrettier,
];

Active rules reference

Here’s what’s actually enforced across the codebase, broken down by package.
RuleSeverityWhat it catches
@typescript-eslint/no-explicit-anywarnUsage of any type — use unknown or a proper type instead
@typescript-eslint/no-unused-varserrorDeclared variables or parameters that are never read
@typescript-eslint/no-require-importserrorrequire() calls — use import instead
@typescript-eslint/ban-ts-commenterror@ts-ignore without a description — use @ts-expect-error with a reason
@typescript-eslint/no-unused-expressionserrorExpressions whose result is never used (e.g. foo && bar as a statement)
@typescript-eslint/no-empty-object-typeerror{} as a type — use object or a specific interface
no-undeferrorReferences to undeclared variables
no-consolewarnDirect console.* calls — use tools.logger instead

Backend-only rules (apps/backend)

RuleSeverityWhat it catches
import/orderwarnImports not grouped or alphabetized (builtin → external → internal → parent → sibling → index → type)

Frontend-only rules (apps/frontend)

RuleSeverityWhat it catches
react-hooks/rules-of-hookserrorHooks called conditionally or outside of a component/custom hook
react-hooks/exhaustive-depswarnMissing dependencies in useEffect, useCallback, or useMemo
react-refresh/only-export-componentswarnNon-component exports from files that Vite’s HMR expects to be component-only

Boiler package-only rules (apps/boiler)

RuleSeverityWhat it catches
@typescript-eslint/explicit-function-return-typewarnFunctions missing an explicit return type annotation

Prettier configuration

Every package with a .prettierrc uses the same settings:
{
  "arrowParens": "avoid",
  "endOfLine": "lf",
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all"
}
A few things worth knowing:
  • arrowParens: "avoid" means x => x instead of (x) => x for single-argument arrow functions.
  • trailingComma: "all" adds trailing commas everywhere valid — including function parameters. This makes multi-line diffs cleaner because adding a new argument doesn’t touch the previous line.
  • printWidth: 100 is a soft limit. Prettier will still wrap at shorter lengths when it makes sense.

Running the linter

# Lint a single package
make lint module=backend
make lint module=frontend

# Lint all packages at once
make lint-all
These commands run inside the Docker container, so you don’t need ESLint installed locally.
💡 Tip: if you’re seeing a lot of import/order errors, run the formatter first — Prettier won’t fix import order, but ESLint can auto-fix it:
# From inside the container (via make exec or similar)
pnpm eslint --fix src/

Wiring ESLint into Cursor

Getting ESLint inline in Cursor means you see problems as you type, not when CI catches them. Here’s how to set it up:

1. Install the ESLint extension

Open Cursor’s extension panel and install ESLint (by Microsoft, dbaeumer.vscode-eslint). It’s the same extension as VS Code.

2. Point it at the right config

Because Refract is a monorepo, you want ESLint to resolve configs relative to the file you’re editing, not the workspace root. Add this to your Cursor settings.json (Cmd+Shift+P → Preferences: Open User Settings (JSON)):
{
  "eslint.workingDirectories": [
    { "pattern": "apps/backend" },
    { "pattern": "apps/frontend" },
    { "pattern": "apps/shared" },
    { "pattern": "apps/tools/*/*" }
  ],
  "eslint.validate": ["typescript", "typescriptreact"],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}
eslint.workingDirectories tells the extension to run ESLint from each package root when editing files in that package — which is what picks up the correct eslint.config.mjs and tsconfig.json. To have Prettier format automatically when you save:
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}
You’ll need the Prettier - Code formatter extension (esbenp.prettier-vscode) installed as well.
⚠️ Watch out: ESLint runs on your host machine here, which means it needs access to the project’s node_modules. Run make pnpm-install first to ensure the workspace deps are installed in the host-accessible node_modules/ folders.

What’s next?