Skip to content

@noctcore/eslint-plugin-contracts

IO boundaries (checked fetch responses, parsed boundary data), error taxonomy, schema and wire naming, env access, money precision and translation keys.

The places a system talks to the outside world are where types stop being true: a fetch whose body is parsed without checking .ok, a JSON.parse(...) as User, an error re-thrown without its cause, process.env.FOO read in forty places, money typed as a float. These rules hold the contract layer to conventions that make those boundaries explicit and checkable.

It will yell at you about naming and boundary discipline: zod schemas must be PascalCaseSchema with a paired inferred type, wire messages need a literal type discriminant, process.env goes through one config module, thrown errors come from an approved taxonomy. Several conventions are opinionated, and the naming rules assume zod. It is a bad fit if you do not use zod, or if your error handling is intentionally untyped. Four rules ship off: three are inert until you give them a sink list, schema file or translation catalogs, and require-schema-parse-at-boundary is a conservative syntactic slice of a concern a type-aware setup does better.

Install
npm install --save-dev @noctcore/eslint-plugin-contracts @typescript-eslint/parser
# or: bun add -D / pnpm add -D @noctcore/eslint-plugin-contracts @typescript-eslint/parser
eslint.config.js
// eslint.config.js
import tsParser from '@typescript-eslint/parser';
import contracts from '@noctcore/eslint-plugin-contracts';
export default [
{
...contracts.configs.recommended,
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tsParser },
},
];
// Rules outside the preset are enabled one by one, e.g.
// { rules: { 'noctcore-contracts/require-schema-parse-at-boundary': 'error' } }

The recommended preset enables 9 of 13 rules.

Each rule links to its page, with the options it takes and Incorrect and Correct examples. A good first read is fetch-must-check-ok.

RuleWhat it reportsPresetFixTypes
env-var-schema-parityRequire every process.env.FOO / import.meta.env.FOO key to be declared in a schema file (.env.example or a zod-env module), so config access and config declaration cannot drift apart.off
fetch-must-check-okRequire a fetch response to be checked with .ok or a status comparison before .json() parses its body.error
money-must-be-decimalDisallow monetary values typed as the JS primitive number. Money-named fields explicitly typed : number lose precision to float rounding; use a Decimal money type instead.error
no-direct-process-envDisallow direct process.env access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use.error
no-error-stringifyDisallow stringifying an error with bare ${error} interpolation, error.toString(), or error + "". These drop the cause chain. Use error instanceof Error ? error.message : String(error) instead.error
require-error-causeRequire re-thrown errors inside a catch to forward the caught error as { cause }. A throw new SomeError(...) that omits the cause severs the chain to the original failure.errorautofix
require-registered-keysRequire the key/name argument of configured sink APIs (storage, event channels, cache keys) to be an imported constant from a registry module, not a raw string literal.off
require-schema-parse-at-boundaryDisallow asserting external boundary data with as T instead of parsing it at runtime. Flags casts of JSON.parse, res.json(), web storage, URL search params, message-event data and LLM tool input, directly or through a const; use a zod/valibot parse.off
restrict-throw-to-taxonomyRestrict throw to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...).error
schema-enum-field-consistencyDisallow a zod field that is an enum in one object schema of a module from being z.string() in another, which widens the wire type every consumer then narrows by hand.error
translation-key-existsRequire every static i18next / react-i18next translation key (t(...), i18n.t(...), <Trans i18nKey>) to exist in the catalog of the namespace in scope.offoptional
wire-message-namingA message-schema const ending in a role suffix (default Event/Command/Query) whose zod object declares type: z.literal(...) must set that literal to kebab-case(const name minus its role suffix).errorautofix
zod-schema-namingEvery exported zod schema is a PascalCase const suffixed Schema, paired with a same-named inferred type (export type Foo = z.infer<typeof FooSchema>).error

Preset: severity in configs.recommended; off means the preset registers the rule switched off, not listed means it leaves the rule out; both are opt-in, so you turn the rule on yourself. Fix: whether the rule ships an autofix or an editor suggestion. Types: whether the rule needs a type-checked program (parserOptions.projectService).