@libar-dev/eslint-plugin-zod-convex
v0.1.0
Published
ESLint rules for zod-convex hybrid validation patterns
Maintainers
Readme
@libar-dev/eslint-plugin-zod-convex
ESLint plugin that enforces the zod-convex hybrid validation pattern — a 3-layer trust boundary model that achieves 90% memory reduction (60-65MB to 10-15MB) in Convex's 64MB runtime.
What is the Hybrid Validation Pattern?
The hybrid validation pattern combines:
- Zod schemas (in
src/validation-schemas/) — Define structure and business rules - Build-time generation (via
@libar-dev/zod-convex-gen) — Convert to Convex validators - Type-only imports — Zero runtime Zod cost, full TypeScript type safety
Installation
npm install --save-dev @libar-dev/eslint-plugin-zod-convexPeer Dependencies:
eslint^8.0.0 || ^9.0.0@typescript-eslint/parser^8.49.0
Quick Start
Enable all recommended rules with a single config:
// eslint.config.mjs
import zodConvex from '@libar-dev/eslint-plugin-zod-convex';
export default [
{
plugins: {
'zod-convex': zodConvex,
},
rules: {
...zodConvex.configs.recommended.rules,
},
},
];Individual Rule Configuration
// eslint.config.mjs
import zodConvex from '@libar-dev/eslint-plugin-zod-convex';
export default [
{
plugins: {
'zod-convex': zodConvex,
},
rules: {
'zod-convex/require-schema-type-aliases': 'error',
'zod-convex/no-runtime-zod-in-convex': 'error',
'zod-convex/require-api-validation': 'error',
'zod-convex/no-layer-3-validation': 'warn',
},
},
];Rules
require-schema-type-aliases
Severity: error | Scope: convex/ directory
Enforces type alias exports from schema files and correct usage in consumer files.
Problem: Using z.infer<typeof Schema> with type-only imports breaks TypeScript compilation.
// WRONG: TypeScript error
import type { MyArgsSchema } from '../../src/validation-schemas/domain/mySchema';
handler: async (ctx, args: z.infer<typeof MyArgsSchema>) => {
// ERROR: MyArgsSchema only refers to a type, but is being used as a value
}Why: The typeof operator requires a runtime value, but import type erases values at compile time.
Solution: Schema files must export type aliases, consumers import the type alias.
// Schema file (src/validation-schemas/domain/mySchema.ts)
export const MyArgsSchema = z.object({
userId: ids.userId(),
data: z.string()
}).strict();
export type MyArgs = z.infer<typeof MyArgsSchema>; // REQUIRED
// Consumer file (convex/domain/myFunction.ts)
import type { MyArgs } from '../../src/validation-schemas/domain/mySchema';
handler: async (ctx, args: MyArgs) => {
// Works perfectly - type alias is compile-time only
}Naming Convention: The rule automatically derives type alias names by removing the Schema suffix:
FooArgsSchema->FooArgsCreateInvoiceArgsSchema->CreateInvoiceArgsUsersTableSchema->User(singular for table schemas)
no-runtime-zod-in-convex
Severity: error | Scope: convex/ directory
Prevents runtime Zod imports in the Convex directory (Layer 2 enforcement).
Problem: Convex runtime has a 64MB memory limit. Including Zod adds 20-30MB to the bundle.
Solution: Use hybrid validation pattern with generated validators + type-only imports.
// WRONG: Runtime Zod in Convex (20-30MB overhead)
import { z } from 'zod';
import { zodToConvex } from '@libar-dev/zod-convex-core';
// RIGHT: Generated validators (zero Zod at runtime)
import type { MyArgs } from '../../src/validation-schemas/domain/mySchema';
import { myArgsFields } from '../generatedValidators/mySchema';
export const myMutation = mutation({
args: myArgsFields,
handler: async (ctx, args: MyArgs) => { ... }
});Architectural Directives: When runtime Zod is architecturally justified (e.g., validating external component responses), use an architectural directive to suppress the warning:
/**
* @architectural-directive: workflow-step-validation
* Reason: Validates external Convex component responses.
* Impact: Requires runtime Zod for parseWithSchema() validation.
*/
import { z } from 'zod';Supported Directives (9 total):
| Category | Directive | Use Case |
|----------|-----------|----------|
| Validation | workflow-step-validation | Validating external component responses |
| Validation | validation-at-boundary | Hybrid pattern with generated validators |
| Infrastructure | schema-infrastructure | Schema factory utilities |
| Infrastructure | workflow-validation-infrastructure | Workflow validation helpers |
| Infrastructure | projection-validation-infrastructure | CQRS projection validation |
| Infrastructure | schema-composition-utility | Zod composition utilities |
| External | ai-response-normalization | AI/external response validation |
| Testing | test-infrastructure | Testing utilities |
| Testing | test-enforcement-safe | Test mocks requiring runtime Zod |
require-api-validation
Severity: error | Scope: app/api/, pages/api/, webhook files
Enforces Zod validation at API route boundaries (Layer 1 enforcement).
Problem: External API boundaries are security-critical. Unvalidated input leads to runtime errors and potential vulnerabilities.
// WRONG: No validation on external input
export async function POST(request: Request) {
const body = await request.json();
await ctx.db.insert('users', body); // Dangerous: unvalidated data
}
// RIGHT: Validate all external input
export async function POST(request: Request) {
const body = await request.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 });
}
await ctx.db.insert('users', result.data);
}Supports Next.js App Router (app/api/**/route.ts), Pages Router (pages/api/**/*.ts), and webhook handlers.
no-layer-3-validation
Severity: warn | Scope: components/, hooks/, utils/, lib/, app/ (excluding app/api/)
Prevents redundant runtime validation in the trusted zone (Layer 3).
Problem: Frontend code should trust already-validated data from API routes (Layer 1) or Convex functions (Layer 2). Redundant validation adds bundle size without security benefit.
// WRONG: Redundant validation in component
function UserCard({ user }: { user: unknown }) {
const result = UserSchema.safeParse(user);
// ...
}
// RIGHT: Trust validated data from API/Convex
import type { User } from '@/src/validation-schemas/users';
function UserCard({ user }: { user: User }) {
// Data already validated at boundary
}Exception: Form validation (react-hook-form, zodResolver) is acceptable for UX progressive enhancement. The rule detects form context and reports as informational instead of a warning.
Known Limitations
require-schema-type-aliases: Does not detect renamed imports (import type { Schema as MySchema }) or namespace imports (import type * as schemas)require-api-validation: Does not track validation in helper functions called by the handler; validation must be directly in the handler bodyno-layer-3-validation: Form detection relies on filename patterns and import heuristicsno-runtime-zod-in-convex: Directive detection scans preceding 1000 characters; very long preambles may not be detected
Related Packages
@libar-dev/zod-convex-gen— Build-time validator generation@libar-dev/zod-convex-core— Runtime Zod-to-Convex conversion@libar-dev/zod-convex-builders— Auth-aware function builders
Contributing
This package is part of the @libar-dev/zod-convex monorepo. See CONTRIBUTING.md for details.
License
MIT
