Always Use ESLint with Convex
Every Convex project should use ESLint with the official @convex-dev/eslint-plugin to catch common mistakes and enforce best practices.
Why ESLint for Convex?
ESLint catches issues that TypeScript can't:
- ❌ Missing
awaiton promises (floating promises) - ❌ Missing argument validators
- ❌ Missing return validators
- ❌ Using
.filter()instead of indexes - ❌ Missing table names in database operations
- ❌ Using
.collect()without pagination - ❌ And more!
Without ESLint, you'll:
- Ship bugs from un-awaited promises
- Deploy functions without validators
- Write slow queries without indexes
- Miss best practices
With ESLint, you'll:
- Catch errors before deployment
- Enforce Convex best practices
- Get auto-fixes for many issues
- Have confidence in your code
Quick Setup
1. Install ESLint Plugin
npm install --save-dev @convex-dev/eslint-plugin
2. Configure ESLint
Modern (Flat Config) - Recommended:
// 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):
// .eslintrc.js
module.exports = {
extends: ["plugin:@convex-dev/recommended"],
rules: {
// Your custom rules
},
};
3. Add Scripts to package.json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit"
}
}
4. Run Lint
# Check for issues
npm run lint
# Auto-fix issues
npm run lint:fix
Essential Convex ESLint Rules
The @convex-dev/eslint-plugin includes these critical rules:
1. No Floating Promises (no-floating-promises)
Catches:
export const createTask = mutation({
handler: async (ctx, args) => {
ctx.db.insert("tasks", args); // ❌ Missing await!
},
});
Fixes to:
export const createTask = mutation({
handler: async (ctx, args) => {
await ctx.db.insert("tasks", args); // ✅ Awaited
},
});
2. Require Argument Validators (require-argument-validators)
Catches:
export const getTask = query({
// ❌ Missing args validator
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
Fixes to:
export const getTask = query({
args: { taskId: v.id("tasks") }, // ✅ Validator added
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
3. Explicit Table IDs (explicit-table-ids)
Catches:
const task = await ctx.db.get(taskId); // ❌ Missing table name
Fixes to:
const task = await ctx.db.get("tasks", taskId); // ✅ Table name added
Note: Convex now requires table names in ctx.db.get(), patch(), replace(), and delete().
4. No Query Collect (no-query-collect)
Catches:
const allTasks = await ctx.db.query("tasks").collect(); // ⚠️ Potentially huge!
Suggests:
// Use pagination for large datasets
const results = await ctx.db.query("tasks").paginate({
cursor: null,
limit: 100,
});
5. Prefer Indexes (prefer-indexes)
Catches:
const user = await ctx.db
.query("users")
.filter(q => q.eq(q.field("email"), email)); // ❌ Slow!
Suggests:
const user = await ctx.db
.query("users")
.withIndex("by_email", q => q.eq("email", email)); // ✅ Fast!
Additional Recommended Rules
Add these TypeScript ESLint rules for Convex:
// 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: "^_",
}],
},
},
];
TypeScript Strict Mode
Enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}
Pre-Commit Hooks
Use Husky + lint-staged to lint before commits:
npm install --save-dev husky lint-staged
npx husky init
// .husky/pre-commit
npm run lint
npm run typecheck
Or with lint-staged for faster commits:
// package.json
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
]
}
}
CI/CD Integration
Add to your CI pipeline:
# .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
IDE Integration
VS Code
Install ESLint extension:
// .vscode/extensions.json
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
Enable auto-fix on save:
// .vscode/settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
Cursor
Same settings as VS Code (uses VS Code engine).
Common ESLint Errors and Fixes
Error: "Promise returned is not awaited"
// ❌ 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);
},
});
Error: "Function is missing argument validators"
// ❌ 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);
},
});
Error: "Missing table name"
// ❌ Error (old style)
await ctx.db.get(taskId);
// ✅ Fix (new style)
await ctx.db.get("tasks", taskId);
Error: "Avoid using .collect() without pagination"
// ❌ Error
const all = await ctx.db.query("tasks").collect();
// ✅ Fix
const results = await ctx.db.query("tasks").paginate({
cursor: null,
limit: 100,
});
Disabling Rules (When Necessary)
Sometimes you need to disable a rule:
// 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!
Troubleshooting
ESLint not finding Convex rules
# Make sure plugin is installed
npm ls @convex-dev/eslint-plugin
# Reinstall if needed
npm install --save-dev @convex-dev/eslint-plugin
Rules not applying to convex/ directory
Make sure your ESLint config includes the convex directory:
export default [
{
files: ["**/*.ts", "**/*.js"],
// Rules apply to all files including convex/
},
];
TypeScript errors in ESLint
Make sure @typescript-eslint/parser is installed:
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
Complete Setup Example
# Install everything
npm install --save-dev \
eslint \
@convex-dev/eslint-plugin \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
prettier \
eslint-config-prettier
// 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,
];
// package.json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"check": "npm run lint && npm run typecheck"
}
}
Why This Matters
Without ESLint:
// 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:
$ 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!
Checklist
- Install
@convex-dev/eslint-plugin - Configure ESLint (flat config or .eslintrc)
- Add lint scripts to package.json
- Enable TypeScript strict mode
- Set up pre-commit hooks
- Configure IDE auto-fix on save
- Add linting to CI/CD pipeline
- Run
npm run lintregularly - Fix all ESLint errors before deploying
Learn More
- Convex ESLint Documentation
- ESLint Setup Guide
- @convex-dev/eslint-plugin on npm
- Convex Best Practices
Remember: ESLint is not optional for production Convex apps. It catches bugs that will slip past TypeScript!