Skip to main content

ESLint rules

ESLint rules for Convex functions enforce best practices. Let us know if there's a rule you would find helpful!

Setup

For ESLint 9 (flat config, using eslint.config.js), install the rules with:

npm i @convex-dev/eslint-plugin --save-dev

and add this to your eslint.config.js file:

import { defineConfig } from "eslint/config";

import convexPlugin from "@convex-dev/eslint-plugin";

export default defineConfig([
// Other configurations

...convexPlugin.configs.recommended,
]);
If you’re using the deprecated .eslintrc.js format

Install these two libraries:

npm i @typescript-eslint/eslint-plugin @convex-dev/eslint-plugin --save-dev

In .eslintrc.js, add:

module.exports =
extends: [
// Other configurations
"plugin:@typescript-eslint/recommended",
"plugin:@convex-dev/recommended",
],
ignorePatterns: ["node_modules/", "dist/", "build/"],
};
If your Convex functions are in a directory other than convex

By default, the Convex ESLint plugin will only apply rules in the convex directory.

If you’re customizing the Convex directory location, here’s how to adapt your ESLint configuration:

// eslint.config.js
import { defineConfig } from "eslint/config";

import convexPlugin from "@convex-dev/eslint-plugin";

const recommendedConfig = convexPlugin.configs.recommended[0];
const recommendedRules = recommendedConfig.rules;

export default defineConfig([
// Other configurations go here...

// Custom configuration with modified directory pattern
{
files: ["**/src/convex/**/*.ts"],
plugins: {
"@convex-dev": convexPlugin,
},
rules: recommendedRules,
},
]);
If you’re using the next lint command from Next.js

For next lint to run ESLint on your convex directory you need to add that directory to the default set of directories. Add this section to your next.config.ts:

const nextConfig: NextConfig = {
/* other options here */

eslint: {
dirs: ["pages", "app", "components", "lib", "src", "convex"],
},
};

Rules

RuleRecommendedAuto-fixable
@convex-dev/no-old-registered-function-syntax
Prefer object syntax for registered functions
🔧
@convex-dev/require-argument-validators
Require argument validators for Convex functions
🔧
@convex-dev/explicit-table-ids
Require explicit table names in database operations
🔧
@convex-dev/import-wrong-runtime
Prevent Convex runtime files from importing from Node runtime files

no-old-registered-function-syntax

Prefer object syntax for registered functions.

Convex queries, mutations, and actions can be defined with a single function or with an object containing a handler property. Using the objects makes it possible to add argument and return value validators, so is always preferable.

// ✅ Allowed by this rule:
export const list = query({
handler: async (ctx) => {
const data = await ctx.db.query("messages").collect();
...
},
});

// ❌ Not allowed by this rule:
export const list = query(async (ctx) => {
const data = await ctx.db.query("messages").collect();
...
});

require-argument-validators

Require argument validators for Convex functions.

Convex queries, mutations, and actions can validate their arguments before beginning to run the handler function. Besides being a concise way to validate, the types of arguments, using argument validators enables generating more descriptive function specs and therefore OpenAPI bindings.

// ✅ Allowed by this rule:
export const list = query({
args: {},
handler: async (ctx) => {
...
},
});

// ✅ Allowed by this rule:
export const list = query({
args: { channel: v.id('channel') },
handler: async (ctx, { channel }) => {
...
},
});

// ❌ Not allowed with option { ignoreUnusedArguments: false } (default)
// ✅ Allowed with option { ignoreUnusedArguments: true }
export const list = query({
handler: async (ctx) => {
...
},
});

// ❌ Not allowed by this rule:
export const list = query({
handler: async (ctx, { channel }: { channel: Id<"channel"> }) => {
...
},
});

This rule can be customized to tolerate functions that don’t define an argument validator but don’t use their arguments. Here’s how you can set up the rule to work this way:

// eslint.config.js

export default defineConfig([
// Your other rules…

{
files: ["**/convex/**/*.ts"],
rules: {
"@convex-dev/require-args-validator": [
"error",
{
ignoreUnusedArguments: true,
},
],
},
},
]);

explicit-table-ids

Require explicit table names in database operations.

Starting from version 1.31.0 of the convex npm package, we recommend including the table name as the first argument to database operations (db.get, db.replace, db.patch, db.delete).

This approach is more secure because it prevents vulnerabilities when an ID from one table is incorrectly typed as belonging to another table. The implicit syntax (where table names are inferred from the ID) will be deprecated in the future to give developers more control over ID generation. For both these reasons, we recommend developers to migrate to the new format.

This rule helps migrate code from the old implicit format to the new explicit format. It uses TypeScript type information to automatically infer the table name from the Id<"tableName"> type and provides automatic fixes.

const messageId: Id<"messages"> = "123" as Id<"messages">;

// ✅ Allowed by this rule:
const message = await ctx.db.get("messages", messageId);
await ctx.db.patch("messages", messageId, { text: "updated" });
await ctx.db.replace("messages", messageId, {
text: "replaced",
author: "Alice",
});
await ctx.db.delete("messages", messageId);

// ❌ Not allowed by this rule:
const message = await ctx.db.get(messageId);
await ctx.db.patch(messageId, { text: "updated" });
await ctx.db.replace(messageId, { text: "replaced", author: "Alice" });
await ctx.db.delete(messageId);

Note that if you’re not using ESLint, you can alternatively use the @convex-dev/codemod CLI tool to automatically migrate to the new format:

npx @convex-dev/codemod@latest explicit-ids

import-wrong-runtime

Prevent Convex runtime files from importing from Node runtime files (files with a "use node" directive).

This rule is experimental. Please let us know if you find it helpful!

// In a file that doesn’t use `"use node"`:

// ✅ Allowed by this rule:
import { someFunction } from "./someOtherFile"; // where someOtherFile doesn't use `"use node"`

// ❌ Not allowed by this rule:
import { someFunction } from "./someNodeFile"; // where someNodeFile uses `"use node"`