Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Always use ESLint with @convex-dev/eslint-plugin to catch Convex-specific issues and enforce best practices
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 160% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 76% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 59% | 0% |
Every Convex project should use ESLint with the official @convex-dev/eslint-plugin to catch common mistakes and enforce best practices.
ESLint catches issues that TypeScript can't:
await on promises (floating promises).filter() instead of indexes.collect() without paginationWithout ESLint, you'll:
With ESLint, you'll:
bashnpm install --save-dev @convex-dev/eslint-plugin
Modern (Flat Config) - Recommended:
javascript// eslint.config.mjs import convexPlugin from "@convex-dev/eslint-plugin"; export default [ // Apply to all files ...convexPlugin.configs.recommended, // Your custom rules { rules: { // Add your overrides here }, }, ];
Legacy (.eslintrc.js):
javascript// .eslintrc.js module.exports = { extends: ["plugin:@convex-dev/recommended"], rules: { // Your custom rules }, };
json{ "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit" } }
bash# Check for issues npm run lint # Auto-fix issues npm run lint:fix
The @convex-dev/eslint-plugin includes these critical rules:
no-floating-promises)Catches:
typescriptexport const createTask = mutation({ handler: async (ctx, args) => { ctx.db.insert("tasks", args); // ❌ Missing await! }, });
Fixes to:
typescriptexport const createTask = mutation({ handler: async (ctx, args) => { await ctx.db.insert("tasks", args); // ✅ Awaited }, });
require-argument-validators)Catches:
typescriptexport const getTask = query({ // ❌ Missing args validator handler: async (ctx, args) => { return await ctx.db.get(args.taskId); }, });
Fixes to:
typescriptexport const getTask = query({ args: { taskId: v.id("tasks") }, // ✅ Validator added handler: async (ctx, args) => { return await ctx.db.get(args.taskId); }, });
explicit-table-ids)Catches:
typescriptconst task = await ctx.db.get(taskId); // ❌ Missing table name
Fixes to:
typescriptconst task = await ctx.db.get("tasks", taskId); // ✅ Table name added
Note: Convex now requires table names in ctx.db.get(), patch(), replace(), and delete().
no-query-collect)Catches:
typescriptconst allTasks = await ctx.db.query("tasks").collect(); // ⚠️ Potentially huge!
Suggests:
typescript// Use pagination for large datasets const results = await ctx.db.query("tasks").paginate({ cursor: null, limit: 100, });
prefer-indexes)Catches:
typescriptconst user = await ctx.db .query("users") .filter(q => q.eq(q.field("email"), email)); // ❌ Slow!
Suggests:
typescriptconst user = await ctx.db .query("users") .withIndex("by_email", q => q.eq("email", email)); // ✅ Fast!
Add these TypeScript ESLint rules for Convex:
javascript// eslint.config.mjs export default [ ...convexPlugin.configs.recommended, { rules: { // Floating promises (critical!) "@typescript-eslint/no-floating-promises": "error", // Misused promises "@typescript-eslint/no-misused-promises": "error", // Require await in async functions "require-await": "error", // No console.log in production "no-console": ["warn", { allow: ["warn", "error"] }], // Enforce return types "@typescript-eslint/explicit-function-return-type": ["warn", { allowExpressions: true, }], // No any (encourage proper typing) "@typescript-eslint/no-explicit-any": "warn", // No unused vars "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", }], }, }, ];
Enable strict mode in tsconfig.json:
json{ "compilerOptions": { "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true } }
Use Husky + lint-staged to lint before commits:
bashnpm install --save-dev husky lint-staged npx husky init
javascript// .husky/pre-commit npm run lint npm run typecheck
Or with lint-staged for faster commits:
json// package.json { "lint-staged": { "*.{ts,tsx,js,jsx}": [ "eslint --fix", "prettier --write" ] } }
Add to your CI pipeline:
yaml# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run lint - run: npm run typecheck
Install ESLint extension:
json// .vscode/extensions.json { "recommendations": [ "dbaeumer.vscode-eslint" ] }
Enable auto-fix on save:
json// .vscode/settings.json { "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ] }
Same settings as VS Code (uses VS Code engine).
typescript// ❌ Error export const create = mutation({ handler: async (ctx, args) => { ctx.db.insert("tasks", args); }, }); // ✅ Fix export const create = mutation({ handler: async (ctx, args) => { await ctx.db.insert("tasks", args); }, });
typescript// ❌ Error export const get = query({ handler: async (ctx, args) => { return await ctx.db.get(args.id); }, }); // ✅ Fix export const get = query({ args: { id: v.id("tasks") }, handler: async (ctx, args) => { return await ctx.db.get(args.id); }, });
typescript// ❌ Error (old style) await ctx.db.get(taskId); // ✅ Fix (new style) await ctx.db.get("tasks", taskId);
typescript// ❌ Error const all = await ctx.db.query("tasks").collect(); // ✅ Fix const results = await ctx.db.query("tasks").paginate({ cursor: null, limit: 100, });
Sometimes you need to disable a rule:
typescript// Disable for one line // eslint-disable-next-line @convex-dev/no-query-collect const all = await ctx.db.query("tasks").collect(); // Disable for whole file /* eslint-disable @convex-dev/no-query-collect */ // Disable in config for specific files export default [ { files: ["convex/migrations/**"], rules: { "@convex-dev/no-query-collect": "off", }, }, ];
⚠️ Warning: Only disable rules when you have a good reason. Most Convex ESLint rules exist to prevent real bugs!
bash# Make sure plugin is installed npm ls @convex-dev/eslint-plugin # Reinstall if needed npm install --save-dev @convex-dev/eslint-plugin
Make sure your ESLint config includes the convex directory:
javascriptexport default [ { files: ["**/*.ts", "**/*.js"], // Rules apply to all files including convex/ }, ];
Make sure @typescript-eslint/parser is installed:
bashnpm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
bash# Install everything npm install --save-dev \ eslint \ @convex-dev/eslint-plugin \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ prettier \ eslint-config-prettier
javascript// eslint.config.mjs import convexPlugin from "@convex-dev/eslint-plugin"; import tseslint from "@typescript-eslint/eslint-plugin"; import tsparser from "@typescript-eslint/parser"; import prettier from "eslint-config-prettier"; export default [ // Convex recommended rules ...convexPlugin.configs.recommended, // TypeScript rules { files: ["**/*.ts", "**/*.tsx"], languageOptions: { parser: tsparser, parserOptions: { project: "./tsconfig.json", }, }, plugins: { "@typescript-eslint": tseslint, }, rules: { "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", }], }, }, // Prettier (must be last) prettier, ];
json// package.json { "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write .", "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck" } }
Without ESLint:
typescript// Ships to production with bugs! export const update = mutation({ handler: async (ctx, args) => { ctx.db.patch(args.id, args.data); // Forgot await console.log("Updated!"); // Logs before update! } });
With ESLint:
bash$ npm run lint convex/tasks.ts 3:5 error Promises must be awaited @typescript-eslint/no-floating-promises ✖ 1 problem (1 error, 0 warnings)
You catch the bug before it reaches production!
@convex-dev/eslint-pluginnpm run lint regularlyRemember: ESLint is not optional for production Convex apps. It catches bugs that will slip past TypeScript!
Other measured skills in the registry, with their headline benchmark lift.