npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

larulefy

v1.2.0

Published

Laravel-style validation for JavaScript

Readme

laRulefy

Laravel-style validation for JavaScript — with real instance isolation.

npm version license

Table of contents

Key features

  • Real per-instance isolation — every createValidator(createXRule) call (and every useValidator()/AngularValidator call in the adapters) allocates a brand-new closure. Two forms on the same page, even using the identical rule declaration, can never see each other's errors or values. No module-level or singleton state anywhere in the core.
  • Laravel-style rule syntax — declare rules with the same pipe syntax Laravel uses ('required|min:3|email'), evaluated by a small built-in parser (parseRuleString.js).
  • Laravel-style messages — built-in and custom messages use Laravel's :attribute/:min/:max-style placeholders, plus optional human-readable attribute labels (Laravel's attributes() convention).
  • Async-capable custom rules — a custom Rule class's passes() may return a boolean or a Promise<boolean>, so an API-backed check (e.g. uniqueness) is a first-class citizen, not a workaround.
  • Layered configuration — global defaults (customMessages, customRules, attributes) set once in laRulefy.config.js are available to every validator, and any single createXRule.js file can override them locally without mutating the shared global config or leaking into a sibling instance.
  • Internationalization — inject a translation table per locale, keyed by rule name so it applies everywhere that rule is used with zero per-field repetition; a runtime locale switch reaches already-created validators on their next validate() call, no remount needed. English is the default and requires no configuration.
  • First-class adapters for Vue, React, and Angular — thin reactivity + lifecycle wrappers only; none of them reimplement validation logic, and all three are exercised by an explicit two-instances-never-share-state isolation test.
  • Framework-agnostic, dependency-free core — createValidator() works in plain Node or the browser with nothing else installed. Vue/React/ Angular are all optional peer dependencies.
  • clearOnUnmount lifecycle integration — opt-in automatic reset() on component teardown across all three adapters (Vue onUnmounted, React effect cleanup, Angular ngOnDestroy).
  • ESM, CJS, and UMD builds — install from npm for a bundler, or drop a <script> tag pointing at the UMD build for zero-build-step usage.
  • 13 built-in rules covering the common Laravel validation basics (required, email, min, max, size, numeric, string, boolean, confirmed, in, regex, array, nullable) — see the full reference.
  • Fully tested — 104 tests across the core and all three adapters, including a real install-matrix compatibility audit (not just changelog reasoning) for each framework's peer dependency floor.

Why laRulefy

Most JS validation libraries keep their state — rule registrations, error bags, sometimes even in-flight validation state — in module-level or otherwise shared storage that's imported once and reused everywhere. That's fine until a page has two forms on it that happen to use the same rule names. Then a change in one form's error state can bleed into the other, because both forms were quietly reading and writing the same shared bucket the whole time.

laRulefy makes that class of bug structurally impossible. Every createValidator(createXRule) call allocates a brand-new closure — errors, current values, everything — and nothing produced by one call is reachable from another. Two components on the same page, even two instances of the same component using the exact same rule declaration, get fully independent validators with no extra bookkeeping required. Rule definitions (required, email, and friends) are pure functions and safe to share; the state produced by validating one specific form is never shared, ever.

The declaration style itself is deliberately modeled on Laravel: instead of writing rules inline, you write a dedicated createXRule.js file — the direct analogue of a Laravel FormRequest — and hand it to createValidator() the way a controller type-hints a FormRequest subclass.

Installation

npm install larulefy

Package page: npmjs.com/package/larulefy

Vue, React, and Angular are all optional peer dependencies — only needed if you import larulefy/vue, larulefy/react, or larulefy/angular respectively. The core has zero runtime dependency on any framework.

Requirements

  • Node.js: any version with native ESM support (Node 14.13+ / any currently-maintained Node release) to build from source; the published package ships pre-built dist/ output, so consuming it doesn't require any particular Node version beyond what your own bundler/runtime needs.
  • Browser: any evergreen browser — the core and adapters use only standard ES2020+ syntax, no browser-specific APIs.
  • Framework versions (only relevant if you use an adapter): see Compatibility for the verified minimum version of Vue/React/Angular.

Module formats & CDN usage

npm run build (via rollup.config.js) produces three formats for the core, and an ESM build per adapter:

| File | Format | Use case | |---|---|---| | dist/larulefy.esm.js | ESM | import { createValidator } from 'larulefy' (bundlers, modern Node) | | dist/larulefy.cjs.js | CommonJS | require('larulefy') | | dist/larulefy.umd.js | UMD, global laRulefy | <script> tag, no build step | | dist/adapters/vue.esm.js | ESM | larulefy/vue | | dist/adapters/react.esm.js | ESM | larulefy/react | | dist/adapters/angular.esm.js | ESM | larulefy/angular |

Zero-build-step usage straight from a <script> tag (e.g. via a CDN that serves npm packages):

<script src="https://unpkg.com/larulefy/dist/larulefy.umd.js"></script>
<script>
  const user = laRulefy.createValidator({
    rules: { email: 'required|email' },
  })
  user.validate({ email: '' }).then(() => console.log(user.email))
</script>

The adapter builds (Vue/React/Angular) are ESM-only, since every framework they target requires a module-aware bundler anyway.

Compatibility

The Vue adapter (larulefy/vue) uses only reactive, onUnmounted, and getCurrentInstance — all present since Vue's first stable 3.0.0 release — so the verified floor is vue@^3.0.0, with no later API requirement pushing it higher.

That floor was verified by actually running test/adapters/vue.test.js against installed copies of each version below, not just reasoned about:

| Vue version | Result | |---|---| | 3.0.0 (earliest 3.x) | ✅ Passing | | 3.2.0 | ✅ Passing | | 3.4.0 | ✅ Passing | | 3.5.39 (latest 3.x) | ✅ Passing |

Vue 2 is not supported. The adapter is built on the Composition API, which Vue 2 doesn't have natively; there's no @vue/composition-api shim or compatibility path in v1.

The React adapter (larulefy/react) uses only useRef, useReducer, and useEffect — all part of the original hooks release, so the verified floor is react@^16.8.0.

That floor was verified by actually running the adapter's isolation/behavior test against installed copies of each version below (via react-test-renderer, pinned to the matching React version per install), not just reasoned about:

| React version | Result | |---|---| | 16.8.0 (earliest hooks release) | ✅ Passing | | 17.0.2 | ✅ Passing | | 18.3.1 | ✅ Passing | | 19.2.7 (latest at time of testing) | ✅ Passing |

On 19.2.7, react-test-renderer itself logs a deprecation notice (React's own test-renderer package is being phased out in favor of @testing-library/react/browser-based testing) — that's a note about the test tool, not the adapter; the adapter itself has no dependency on react-test-renderer and works identically across all four versions above.

The Angular adapter (larulefy/angular) uses Injectable, InjectionToken, Injector, inject, and signal from @angular/core. signal() itself has been stable since Angular 17.0.0 — but that is not what sets the verified floor below.

That floor was verified the same way as Vue and React: actually running test/adapters/angular.test.js against installed copies of each version below in isolated scratch installs, not just reasoned about from the signals changelog:

| Angular version | Result | |---|---| | 16.2.12 (signal() in developer preview) | ❌ Failing | | 17.0.0 (signal() stabilized) | ❌ Failing | | 18.0.0 | ❌ Failing | | 19.0.0 | ❌ Failing | | 20.0.0 | ✅ Passing | | 22.0.6 (latest at time of testing) | ✅ Passing |

Verified floor: @angular/core@^20.0.0. Every version from 16 through 19 fails identically at TestBed.createComponent() with NullInjectorError: No provider for InjectionToken compilerOptions! — a TestBed/JIT-compiler wiring issue combining platformBrowserTesting() with a Vitest + happy-dom environment, unrelated to signal(), which Angular 20's TestBed rework resolves. The original ^16.0.0 guess (reasoned only from "Signals require 16+") undershot the real floor by four majors — this is exactly why Part 13 requires an actual install matrix instead of changelog reasoning alone.

@angular/compiler, @angular/platform-browser, and @angular/common must all be pinned to the exact same version as @angular/core (their own peerDependencies enforce this at 20.0.0+); zone.js@^0.16.2 and rxjs@^7.8.2 (this repo's devDependency versions) both satisfy the verified floor's peer ranges.

Quick start — vanilla JS

// createUserRule.js — pure declaration, no runtime state
export default {
  rules: {
    name: 'required|min:3',
    email: 'required|email',
    age: 'nullable|numeric|min:18',
  },
  messages: {
    'name.required': 'Please enter your name.',
  },
}
import { createValidator } from 'larulefy'
import createUserRule from './createUserRule.js'

const user = createValidator(createUserRule)

await user.validate({ name: '', email: 'not-an-email' })
console.log(user.name)   // "Please enter your name."
console.log(user.email)  // "The email must be a valid email address."

user.reset()
console.log(user.name)   // ""

validate() is async because custom rules are allowed to do async work (e.g. an API call to check uniqueness) — see Custom rules.

Quick start — Vue

<script setup>
import { ref } from 'vue'
import { useValidator } from 'larulefy/vue'
import createUserRule from './createUserRule.js'

const name = ref('')
const email = ref('')
const user = useValidator(createUserRule)

function currentValues() {
  return { name: name.value, email: email.value }
}

async function onSubmit() {
  const passed = await user.validate(currentValues())
  if (passed) {
    // proceed — e.g. submit to an API
  }
}

function onClear() {
  name.value = ''
  email.value = ''
  user.reset()
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <label>
      Name
      <input v-model="name" @blur="user.validate('name', currentValues())" />
      <span v-if="user.name" class="error">{{ user.name }}</span>
    </label>

    <label>
      Email
      <input v-model="email" @blur="user.validate('email', currentValues())" />
      <span v-if="user.email" class="error">{{ user.email }}</span>
    </label>

    <button type="submit">Sign up</button>
    <button type="button" @click="onClear">Clear</button>
  </form>
</template>

useValidator(createXRule) must be called once per component, directly inside setup()/<script setup> (the normal composable rule). That's enough for isolation — each setup() call gets its own instance, the same way it does in plain JS. This works with no other setup beyond importing your laRulefy.config.js once at the app's entry point.

Quick start — React

import { useState } from 'react'
import { useValidator } from 'larulefy/react'
import createUserRule from './createUserRule.js'

function SignupForm() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const user = useValidator(createUserRule)

  async function onSubmit(e) {
    e.preventDefault()
    const passed = await user.validate({ name, email })
    if (passed) {
      // proceed — e.g. submit to an API
    }
  }

  function onClear() {
    setName('')
    setEmail('')
    user.reset()
  }

  return (
    <form onSubmit={onSubmit}>
      <label>
        Name
        <input
          value={name}
          onChange={(e) => setName(e.target.value)}
          onBlur={() => user.validate('name', { name, email })}
        />
        {user.name && <span className="error">{user.name}</span>}
      </label>

      <label>
        Email
        <input
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          onBlur={() => user.validate('email', { name, email })}
        />
        {user.email && <span className="error">{user.email}</span>}
      </label>

      <button type="submit">Sign up</button>
      <button type="button" onClick={onClear}>Clear</button>
    </form>
  )
}

useValidator(createXRule) must be called directly in the component's function body (the normal rule-of-hooks call site) — the core instance is created exactly once per component instance via useRef, never re-created on re-render, and reading user.<field> after validate()/reset() always reflects the latest state on the next render. This works with no other setup beyond importing your laRulefy.config.js once at the app's entry point.

Quick start — Angular

// signup-form.component.ts
import { Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { AngularValidator, CREATE_X_RULE } from 'larulefy/angular'
import createUserRule from './createUserRule.js'

@Component({
  standalone: true,
  selector: 'signup-form',
  imports: [FormsModule],
  providers: [
    { provide: CREATE_X_RULE, useValue: createUserRule },
    AngularValidator,
  ],
  template: `
    <form (ngSubmit)="onSubmit()">
      <label>
        Name
        <input [(ngModel)]="name" name="name" (blur)="user.validate('name', { name, email })" />
        <span *ngIf="user.name()" class="error">{{ user.name() }}</span>
      </label>

      <label>
        Email
        <input [(ngModel)]="email" name="email" (blur)="user.validate('email', { name, email })" />
        <span *ngIf="user.email()" class="error">{{ user.email() }}</span>
      </label>

      <button type="submit">Sign up</button>
      <button type="button" (click)="onClear()">Clear</button>
    </form>
  `,
})
export class SignupFormComponent {
  user = inject(AngularValidator)
  name = ''
  email = ''

  async onSubmit() {
    const passed = await this.user.validate({ name: this.name, email: this.email })
    if (passed) {
      // proceed — e.g. submit to an API
    }
  }

  onClear() {
    this.name = ''
    this.email = ''
    this.user.reset()
  }
}

Two things are different from the Vue/React adapters, both deliberate:

  • Each field is a Signal, not a plain property — read it as user.name(), a function call, not user.name. Angular is moving toward Signals for reactivity outside of Zone.js (zoneless apps), so the adapter exposes Signals directly rather than a Zone.js-dependent plain property that would silently stop updating templates in a zoneless app.
  • AngularValidator must be listed in the consuming component's own providers: [...] array, alongside a CREATE_X_RULE provider for your rule declaration — never register it at the root injector (providedIn: 'root') or anywhere above component scope (an NgModule, a lazy route's environment injector). Component-level providers is the only level in Angular's DI hierarchy that creates a fresh instance per component instance rather than per some broader shared scope — see the comment above AngularValidator in src/adapters/angular.js for the full comparison against the alternatives. Get this wrong and two components on the same page silently share one instance's errors, the exact bug this library exists to prevent.

For consumers who'd rather not lean on Angular's DI at all, a plain factory is also available as an escape hatch:

import { createAngularValidator } from 'larulefy/angular'
import createUserRule from './createUserRule.js'

export class SignupFormComponent {
  user = createAngularValidator(createUserRule)
}

clearOnUnmount is wired to ngOnDestroy() — when true, AngularValidator calls reset() automatically when the providing component is destroyed, the same role Vue's onUnmounted and React's effect-cleanup play.

Rule syntax reference

Rules use Laravel's pipe syntax:

rules: {
  name: 'required|min:3',
  email: 'required|email|max:255',
}

Built-in rules (v1):

| Rule | Description | |---|---| | required | Value must be present (non-empty string/array, not null/undefined). | | nullable | If the value is empty, skip the rest of that field's pipeline. | | email | Must be a valid email address. | | min:n | Minimum string length / array length / numeric value. | | max:n | Maximum string length / array length / numeric value. | | size:n | Exact string length / array length / numeric value. | | numeric | Must be a number or numeric string. | | string | Must be a string. | | boolean | Must be true/false/0/1/'0'/'1'. | | confirmed | Must match a sibling <field>_confirmation value. | | in:a,b,c | Must be one of the given values. | | regex:pattern | Must match the pattern (raw source, or /pattern/flags). | | array | Must be an array. |

Every rule except required and nullable passes automatically when the value is empty — presence is required's job, matching Laravel's own behavior.

Custom rules

For anything beyond a pure function — a uniqueness check against an API, for example — a createXRule.js file can register a custom Rule class, following Laravel's own Rule contract:

export class Unique {
  async passes(attribute, value) {
    const res = await fetch(`/api/check-email?email=${value}`)
    return (await res.json()).available
  }
  message() {
    return 'The :attribute has already been taken.'
  }
}
// createUserRule.js
import { Unique } from './rules/Unique.js'

export default {
  rules: { email: 'required|email|unique' },
  customRules: { unique: Unique },
}

passes(attribute, value) may return a boolean or a Promise<boolean> — async rules are fully supported, and validate() always returns a promise so sync and async rules can be mixed freely. message() returns a Laravel-style template string (:attribute is substituted automatically).

Custom rules registered globally via customRules in laRulefy.config.js are available to every createXRule.js file without re-importing them; a rule of the same name registered locally in a specific declaration takes precedence for that declaration only.

Attribute labels

By default, :attribute in a message is substituted with the raw field name (email, date_of_birth, ...). To show a friendlier, human-readable label instead — Laravel's attributes() convention — set attributes on the createXRule.js declaration, the global config, or both:

// createUserRule.js
export default {
  rules: { email: 'required|email' },
  attributes: { email: 'Email Address' },
}
await user.validate({ email: '' })
console.log(user.email) // "The Email Address field is required."

attributes in laRulefy.config.js sets a global default label per field name, available to every createXRule.js file; a declaration's own attributes overrides the global label for that declaration only, the same layering rule messages/customMessages already follow.

Configuration reference

// laRulefy.config.js
import { defineConfig } from 'larulefy'

export default defineConfig({
  locale: 'en',
  fallbackLocale: 'en',
  locales: {},
  lang: 'js',
  clearOnUnmount: true,
  errorFormat: 'laravel',
  customMessages: {},
  customRules: {},
  attributes: {},
})

| Key | Description | |---|---| | locale | Active locale used to look up translated built-in/global messages (see Internationalization). Defaults to 'en', which needs no locales entry — the built-in rule messages already are the English table. | | fallbackLocale | Locale to fall back to when locale's table is missing a translation for a given rule, before falling back to the built-in English message. Defaults to 'en'. | | locales | Injected translation tables, keyed by locale then by rule name (required) or 'field.rule' for a field-specific override within that locale ('email.required'). Empty by default — English behavior is unchanged unless you opt in. | | supportedLocales | Informational list of locales the app offers, e.g. to drive a language switcher. Defaults to 'en' plus the keys of locales; set it explicitly if you want a different list. | | lang | Reserved placeholder, intentionally a permanent no-op — confirmed with the maintainer (2026-07-16) that it should ship this way rather than be defined or removed, so a future need distinct from locale has a home without a breaking change. It doesn't affect behavior. | | clearOnUnmount | Consumed by framework adapters (not the core): when true, the Vue, React, and Angular adapters all call reset() automatically at that framework's unmount/destroy equivalent. | | errorFormat | Message convention for built-in/default messages. Only 'laravel' is supported. | | customMessages | Global message overrides, Laravel-style ('field.rule' → message string). Available as defaults to every instance; a declaration's own messages overrides these without mutating the global config. Checked before locale translations. | | customRules | Global custom Rule registry, available to every createXRule.js file by name. | | attributes | Global human-readable field labels substituted for :attribute in messages (see Attribute labels). Available as defaults to every instance; a declaration's own attributes overrides these without mutating the global config. |

Internationalization

By default every built-in rule message is English, and nothing changes unless you opt in. To support other languages, inject a translation table per locale — keyed by rule name, so one entry covers that rule for every field in the app, with no per-field repetition:

// laRulefy.config.js
import { defineConfig } from 'larulefy'

export default defineConfig({
  locale: 'ar',
  fallbackLocale: 'en',
  locales: {
    ar: {
      required: ':attribute مطلوب.',
      email: ':attribute يجب أن يكون بريدًا إلكترونيًا صالحًا.',
      // 'field.rule' overrides a single field within this locale only:
      'password.confirmed': 'تأكيد كلمة المرور غير متطابق.',
    },
  },
})

Every createValidator/useValidator instance reads locale fresh on each validate() call — switching locale at runtime with another defineConfig({ locale: '...' }) call reaches instances that were already created, with no remount needed. Resolution order per failing rule is: a declaration's own messages → global customMessages → the active locale's 'field.rule' entry → the active locale's rule-keyed entry → the same two lookups against fallbackLocale → the built-in English message.

API reference

| Call | Behavior | |---|---| | createValidator(createXRule) | Core factory. Returns a brand-new, fully isolated instance every call. | | useValidator(createXRule) (from larulefy/vue) | Same instance, wrapped in Vue reactivity, with clearOnUnmount wired to the component lifecycle. | | useValidator(createXRule) (from larulefy/react) | Same instance, wrapped in a React hook that re-renders the component on validate()/reset(), with clearOnUnmount wired to unmount cleanup. | | AngularValidator + CREATE_X_RULE (from larulefy/angular) | Same instance, wrapped in an Angular DI-scoped service — provide both in the consuming component's own providers: [...], then inject(AngularValidator). clearOnUnmount wired to ngOnDestroy(). Fields are Signals: instance.field(). | | createAngularValidator(createXRule) (from larulefy/angular) | Non-DI escape hatch — same AngularValidator shape, constructed directly without relying on Angular's component-tree injector. | | instance.<field> | Flat read of that field's current error message, or '' if it has none. Angular: instance.<field>() (a Signal). | | instance.validate(values) | Validates every declared field against values. Returns Promise<boolean>. | | instance.validate('field') | Re-validates just that field, reusing the last-provided values. | | instance.validate('field', values) | Updates the stored values, then validates just that field. | | instance.reset() | Clears every field's error state and any stored values. | | defineConfig(config) | Merges config over the library defaults and sets it as the active global config. |

Framework support

| Framework | Status | |---|---| | Vue | Supported now, via larulefy/vue (optional peer dependency). Requires vue@^3.0.0 — see Compatibility. Vue 2 is not supported. | | React | Supported now, via larulefy/react (optional peer dependency). Requires react@^16.8.0 — see Compatibility. | | Angular | Supported now, via larulefy/angular (optional peer dependency). Fields are exposed as Signals (instance.field()) rather than plain properties — see Quick start — Angular. See Compatibility for the verified version floor. | | Svelte | Not yet — same as above. | | Vanilla / no framework | Fully supported today — just use createValidator() directly, as shown above. |

TypeScript support

The source is plain JavaScript with JSDoc annotations (see the coding standards: JSDoc over TypeScript, no build step for types). Editors that understand JSDoc (VS Code, WebStorm, ...) get inline type hints and autocomplete for every exported function automatically. There is no bundled .d.ts declaration file yet — if you need strict .ts typings, treat this as an open contribution area (see Contributing).

Contributing

git clone https://github.com/mohammedalbadry/laRulefy.git
cd laRulefy
npm install
npm test    # run the full suite (core + Vue + React + Angular adapters)
npm run build
  • The core suite runs with zero framework installed — don't add Vue/ React/Angular as a dependency of anything under test/core/. Framework-specific tests live under test/adapters/<framework>.test.js, the only place that framework's package may be a test dependency.
  • The single most important test for any adapter change: mount two component instances from the same createXRule.js declaration, put one into an invalid state, and assert the other is completely unaffected. See the existing isolation tests in test/adapters/ for the pattern.
  • Run npm test and npm run build before opening a PR. PRs welcome — bug reports, new built-in rules, and additional framework adapters (Svelte is on the radar, see Framework support) are all welcome.

License

MIT — see LICENSE.