Skip to content

cancatma-hola/eslint-plugin-nextjs-server-actions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

eslint-plugin-nextjs-server-actions

ESLint rules for safe Next.js server actions and Prisma bulk operations.

Battle-tested on a production multi-tenant B2B platform. These rules catch classes of bugs that TypeScript won't — silent result discards and unconditional bulk operations that wipe entire tables.

Install

npm install --save-dev eslint-plugin-nextjs-server-actions

Setup (ESLint Flat Config)

// eslint.config.mjs
import nextjsServerActions from "eslint-plugin-nextjs-server-actions";

export default [
  nextjsServerActions.configs.recommended,
  // ... your other configs
];

Or enable rules individually:

import nextjsServerActions from "eslint-plugin-nextjs-server-actions";

export default [
  {
    plugins: { "nextjs-server-actions": nextjsServerActions },
    rules: {
      "nextjs-server-actions/no-dangerous-prisma": "error",
      "nextjs-server-actions/no-unchecked-action-result": "error",
      "nextjs-server-actions/enforce-try-action": "warn",
    },
  },
];

Rules

no-dangerous-prisma ✅ recommended

Prevents unconditional Prisma bulk operations that silently wipe entire database tables.

Blocked patterns:

// ❌ no WHERE clause — deletes ALL rows
await prisma.user.deleteMany();
await prisma.order.deleteMany({});
await prisma.product.deleteMany({ where: {} });

// ❌ same for updateMany
await prisma.user.updateMany();

// ❌ dangerous raw SQL
await prisma.$executeRaw`DROP TABLE "User"`;
await prisma.$executeRawUnsafe("TRUNCATE orders");

Correct patterns:

// ✅ always provide a real WHERE condition
await prisma.user.deleteMany({ where: { tenantId: companyId } });
await prisma.session.deleteMany({ where: { expiresAt: { lt: new Date() } } });

Disable for seed scripts / test fixtures only:

// eslint-disable-next-line nextjs-server-actions/no-dangerous-prisma
await prisma.user.deleteMany();

no-unchecked-action-result ✅ recommended

Next.js server actions wrapped in a try/catch helper return { error?: string } instead of throwing. Discarding the return value silently swallows all errors.

Blocked patterns:

import { createOrder } from "@/app/actions/order";

// ❌ result discarded — errors are silently lost
await createOrder(payload);
void createOrder(payload);

Correct patterns:

// ✅ capture and handle the result
const result = await createOrder(payload);
if (result?.error) {
  toast(result.error, "error");
  return;
}

Scope: Only applies to functions imported from action modules (@/app/actions/**, ../actions/, ./actions/). Files with "use server" directive are excluded (they are the action definitions, not callers).


enforce-try-action ⚠️ warning

Requires all exported async functions in "use server" files to be wrapped in a tryAction() call. Prevents unhandled exceptions from leaking raw server errors to the client.

Example tryAction helper:

// lib/try-action.ts
export async function tryAction<T>(
  fn: () => Promise<T>
): Promise<{ data?: T; error?: string }> {
  try {
    return { data: await fn() };
  } catch (err) {
    console.error(err);
    return { error: err instanceof Error ? err.message : "Unexpected error" };
  }
}

Blocked patterns:

"use server";
// ❌ unhandled exception exposed to client
export async function deleteProduct(id: string) {
  await prisma.product.delete({ where: { id } });
}

Correct patterns:

"use server";
// ✅ errors caught and normalized
export async function deleteProduct(id: string) {
  return tryAction(async () => {
    await prisma.product.delete({ where: { id } });
  });
}

Configurable wrapper name:

// eslint.config.mjs
{
  rules: {
    "nextjs-server-actions/enforce-try-action": ["warn", { wrapperName: "withAction" }],
  },
}

Why these rules exist

In a multi-tenant B2B platform, the most dangerous bugs are the ones that don't throw — they just silently do the wrong thing:

  • A deleteMany() without a where clause empties the whole table; Prisma's TypeScript types don't prevent it.
  • A await someAction() without capturing the result means users never see error messages; the action failed, but the UI proceeds as if it succeeded.
  • An unguarded async server action lets stack traces and database errors leak to client responses.

These rules encode lessons from production incidents into compile-time checks.


License

MIT

About

ESLint rules for safe Next.js server actions and Prisma bulk operations

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors