@noctcore/eslint-plugin-contracts
v0.7.2
Published
ESLint rules for IO contracts and config: checked fetch responses, parsed boundary data, error cause and taxonomy, zod schema and wire naming, env access, decimal money and translation keys.
Maintainers
Readme
@noctcore/eslint-plugin-contracts
Rules for shared contract conventions: zod schema naming, wire-message discriminants, error
stringification, direct process.env access and money precision, where drift between producer and
consumer breaks at runtime.
Docs: noctcore.github.io/eslint-plugins/packages/contracts
Not a good fit for a codebase that does not use zod, or whose error handling is intentionally untyped.
Requirements
- ESLint 9 or newer, flat config (
eslint.config.js) only. configs.recommendedregisters the plugin and sets rule severities, nothing else. It sets nofilesand no parser, so it applies to whatever files the rest of your config lints. To lint TypeScript, add afilespattern and@typescript-eslint/parser, as in the quick start.
Install
bun add -D @noctcore/eslint-plugin-contracts @typescript-eslint/parser # or npm i -D / pnpm add -DQuick start
// 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 },
},
// Optional: tune a preset rule's options.
{
files: ['**/*.{ts,tsx}'],
rules: {
'noctcore-contracts/zod-schema-naming': ['error', { roleSuffixes: ['Event', 'Command', 'Query'] }],
'noctcore-contracts/no-direct-process-env': ['error', { configModule: '@acme/config' }],
},
},
];Opt-in rules
Four rules ship off: require-registered-keys, env-var-schema-parity and
translation-key-exists do nothing until their sinks / schema / catalogs options are set, and
require-schema-parse-at-boundary is a conservative syntactic slice of a type-aware concern.
// eslint.config.js
export default [
// ...the quick start's entries, then:
{
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tsParser },
rules: {
'noctcore-contracts/require-registered-keys': ['error', {
sinks: [{ callee: 'localStorage.getItem', argIndex: 0 }],
}],
'noctcore-contracts/env-var-schema-parity': ['error', { schema: '.env.example' }],
'noctcore-contracts/translation-key-exists': ['error', {
catalogs: [{ file: 'locales/en/common.json', namespace: 'common' }],
}],
'noctcore-contracts/require-schema-parse-at-boundary': 'error',
},
},
];Rules
✅ on in recommended (error) · 🔘 opt-in: not on in recommended, enable it yourself · ⚙️ needs options · 🔧 --fix · 💡 suggestions · 💭 needs type info · ❌ deprecated
| Rule | Description | Preset | ⚙️ | 🔧 | 💡 | 💭 |
| --- | --- | :-: | :-: | :-: | :-: | :-: |
| env-var-schema-parity | Require 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. | 🔘 | ⚙️ | | | |
| fetch-must-check-ok | Require a fetch response to be checked with .ok or a status comparison before .json() parses its body. | ✅ | | | | |
| money-must-be-decimal | Disallow 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. | ✅ | | | | |
| no-direct-process-env | Disallow direct process.env access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use. | ✅ | | | | |
| no-error-stringify | Disallow 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. | ✅ | | | | |
| require-error-cause | Require 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. | ✅ | | 🔧 | | |
| require-registered-keys | Require 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. | 🔘 | ⚙️ | | | |
| require-schema-parse-at-boundary | Disallow 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. | 🔘 | | | | |
| restrict-throw-to-taxonomy | Restrict throw to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...). | ✅ | | | | |
| schema-enum-field-consistency | Disallow 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. | ✅ | | | | |
| translation-key-exists | Require every static i18next / react-i18next translation key (t(...), i18n.t(...), <Trans i18nKey>) to exist in the catalog of the namespace in scope. | 🔘 | ⚙️ | | | |
| wire-message-naming | A 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). | ✅ | | 🔧 | | |
| zod-schema-naming | Every exported zod schema is a PascalCase const suffixed Schema, paired with a same-named inferred type (export type Foo = z.infer<typeof FooSchema>). | ✅ | | | | |
Severity policy
Every rule is error or off, never warn: a warning is a rule nobody obeys. See the
severity policy.
