| name | cursor-plugin-convex-rule-use-eslint-always |
| description | Always use ESLint with @convex-dev/eslint-plugin to catch Convex-specific issues and enforce best practices |
| metadata | {"version":"0.1.0"} |
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
await on 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:
import convexPlugin from "@convex-dev/eslint-plugin";
export default [
...convexPlugin.configs.recommended,
{
rules: {
},
},
];
Legacy (.eslintrc.js):
module.exports = {
extends: ["plugin:@convex-dev/recommended"],
rules: {
},
};
3. Add Scripts to package.json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit"
}
}
4. Run Lint
npm run lint
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);
},
});
Fixes to:
export const createTask = mutation({
handler: async (ctx, args) => {
await ctx.db.insert("tasks", args);
},
});
2. Require Argument Validators (require-argument-validators)
Catches:
export const getTask = query({
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
Fixes to:
export const getTask = query({
args: { taskId: v.id("tasks") },
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);
Fixes to:
const task = await ctx.db.get("tasks", taskId);
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();
Suggests:
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));
Suggests:
const user = await ctx.db
.query("users")
.withIndex("by_email", q => q.eq("email", email));
Additional Recommended Rules
Add these TypeScript ESLint rules for Convex:
export default [
...convexPlugin.configs.recommended,
{
rules: {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"require-await": "error",
"no-console": ["warn", { allow: ["warn", "error"] }],
"@typescript-eslint/explicit-function-return-type": ["warn", {
allowExpressions: true,
}],
"@typescript-eslint/no-explicit-any": "warn",
"@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
npm run lint
npm run typecheck
Or with lint-staged for faster commits:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
]
}
}
CI/CD Integration
Add to your CI pipeline:
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:
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
Enable auto-fix on save:
{
"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"
export const create = mutation({
handler: async (ctx, args) => {
ctx.db.insert("tasks", args);
},
});
export const create = mutation({
handler: async (ctx, args) => {
await ctx.db.insert("tasks", args);
},
});
Error: "Function is missing argument validators"
export const get = query({
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const get = query({
args: { id: v.id("tasks") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
Error: "Missing table name"
await ctx.db.get(taskId);
await ctx.db.get("tasks", taskId);
Error: "Avoid using .collect() without pagination"
const all = await ctx.db.query("tasks").collect();
const results = await ctx.db.query("tasks").paginate({
cursor: null,
limit: 100,
});
Disabling Rules (When Necessary)
Sometimes you need to disable a rule:
const all = await ctx.db.query("tasks").collect();
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
npm ls @convex-dev/eslint-plugin
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"],
},
];
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
npm install --save-dev \
eslint \
@convex-dev/eslint-plugin \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
prettier \
eslint-config-prettier
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 [
...convexPlugin.configs.recommended,
{
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,
];
{
"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:
export const update = mutation({
handler: async (ctx, args) => {
ctx.db.patch(args.id, args.data);
console.log("Updated!");
}
});
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
Learn More
Remember: ESLint is not optional for production Convex apps. It catches bugs that will slip past TypeScript!