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

fluentv

v1.0.0

Published

Selector-based TypeScript validation backed by fluentvalidation-ts

Downloads

319

Readme

fluentv

Selector-based validation API for TypeScript with zero runtime dependencies. The fluentvalidation-ts 5.0.0 engine is stored as a local snapshot and shipped inside the package. Built-in rules, messages, conditions, custom predicates, and child validators are executed by that engine; the fluentv code handles selectors, nested paths, and error assembly.

This is an early, tested foundation, not a claim that this new library is mature. Upstream: https://github.com/AlexJPotter/fluentvalidation-ts (Apache-2.0).

Breaking change in 1.0.0

0.1.x returned the nested error tree directly from validate():

// fluentv 0.1.x
const errors = validator.validate(model);
// { name: 'Value cannot be empty', address: { city: 'City is required' } }

1.0.0 changes the shape of that result and adds stable error codes:

| 0.1.x | 1.0.0 | | --- | --- | | validate() → nested tree | validate() → flat ValidationResult | | validateAsync() → nested tree | validateAsync() → flat ValidationResult | | — | validateTree() / validateTreeAsync() → nested tree (old behavior) | | — | validateAndThrow() / validateAndThrowAsync() → throws ValidationException | | — | isValid, hasErrors, ValidationResult, ValidationException, ErrorTree |

Migration is mechanical:

// Keep the old nested output:
const tree = validator.validateTree(model);

// Or adopt the flat result with stable codes:
const result = validator.validate(model);
if (!result.isValid) {
  result.errors;        // [{ propertyName: 'email', errorCode: 'notEmpty', errorMessage: '...' }]
  result.toDictionary(); // { email: ['...'] }
}

The flat errorCode values are stable across message changes and translations, so API clients and tests no longer have to depend on human-readable text.

Example

import { Validator } from 'fluentv';

type User = {
  name: string;
  age: number;
  password: string;
  confirmation: string;
  address?: { city: string } | null;
};

class UserValidator extends Validator<User> {
  constructor() {
    super();
    this.ruleFor(u => u.name).notEmpty().maxLength(100);
    this.ruleFor(u => u.age).greaterThanOrEqualTo(18);
    this.ruleFor(u => u.address?.city)
      .notUndefined()
      .notEmpty()
      .withMessage('City is required');
    this.ruleFor(u => u.confirmation)
      .must((value, model) => value === model.password)
      .withMessage('Passwords do not match');
  }
}

const validator = new UserValidator();
const result = validator.validate({
  name: '', age: 20, password: 'secret', confirmation: 'secret',
  address: { city: '' },
});
result.isValid; // false
result.errors;
// [
//   { propertyName: 'name', errorCode: 'notEmpty', errorMessage: 'Value cannot be empty' },
//   { propertyName: 'address.city', errorCode: 'notEmpty', errorMessage: 'City is required' },
// ]
result.toDictionary(); // { name: ['Value cannot be empty'], 'address.city': ['City is required'] }

// The nested shape is still available separately:
validator.validateTree({ name: '', age: 20, password: 'secret', confirmation: 'secret', address: { city: '' } });
// { name: 'Value cannot be empty', address: { city: 'City is required' } }

ruleFor(u => u.name) infers string, so numeric rules cannot be used on it. The original model is passed to must, when, unless, and the setValidator factory. Child validators may be a Validator from this package or any other implementation exposing validateTree / validate in the same shape.

Async

import { AsyncValidator } from 'fluentv';

const validator = new AsyncValidator<{ email: string }>();
validator.ruleFor(u => u.email)
  .emailAddress()
  .mustAsync(async email => email !== '[email protected]')
  .withMessage('Email already in use');

const result = await validator.validateAsync({ email: '[email protected]' });
// result.errors === [{ propertyName: 'email', errorCode: 'mustAsync', errorMessage: 'Email already in use' }]

Collections

const validator = new Validator<{
  tags: readonly string[];
  data?: { scores: number[] };
}>();

validator.ruleForEach(m => m.tags).notEmpty().maxLength(20);
validator.ruleForEach(m => m.data?.scores).greaterThanOrEqualTo(0);

validator.validateTree({ tags: ['ok', ''], data: { scores: [10, -1] } });
// {
//   tags: [null, 'Value cannot be empty'],
//   data: { scores: [null, 'Value must be greater than or equal to 0'] }
// }

validator.validate({ tags: ['ok', ''], data: { scores: [10, -1] } }).errors;
// [
//   { propertyName: 'tags[1]', errorCode: 'notEmpty', errorMessage: 'Value cannot be empty' },
//   { propertyName: 'data.scores[1]', errorCode: 'greaterThanOrEqualTo', errorMessage: '...', placeholders: { value: 0 } }
// ]

Rule types follow the element type, including readonly and nullable arrays. Empty/null/undefined collections are skipped; use ruleFor(...).notUndefined() / .notNull() to make a collection required. Non-array values are rejected at runtime. ruleForEach keeps element positions: valid elements produce null, and a collection with no errors is omitted.

setValidator supports object elements. AsyncValidator.ruleForEach also supports mustAsync and setAsyncValidator. Conditions are checked per element, and callbacks receive the original parent model. Elements are processed in order; several validateAsync calls may run concurrently.

Validation results

validate() (and validateAsync()) return a flat ValidationResult:

interface ValidationFailure {
  readonly propertyName: string; // "email", "address.city", "items[0].name", "$" for an object-level error
  readonly errorMessage: string;
  readonly errorCode: string;    // stable code: "notEmpty", "maxLength", "must", "setValidator", ...
  readonly placeholders?: Readonly<Record<string, unknown>>; // e.g. { max: 100 }
}

class ValidationResult {
  readonly errors: readonly ValidationFailure[];
  get isValid(): boolean;
  toDictionary(): Record<string, string[]>; // property -> messages
}
  • errorCode is the rule name and stays stable even when the message is replaced via withMessage or translated. placeholders is populated for parameterized rules (maxLength, minLength, length, numeric comparisons, precisionScale, equal/notEqual, matches).
  • Rules run through the upstream engine. By default cascade = 'continue', so every failing rule is reported (for example notEmpty().minLength(5) on an empty string yields two failures). A Validator subclass can set this.cascade = 'stop' to stop at the first failing rule per chain.
  • Quick checks: isValid(resultOrTree) and hasErrors(resultOrTree). For a ValidationResult, use the result.isValid getter.
  • validateAndThrow(model) / validateAndThrowAsync(model) throw a ValidationException carrying .errors when invalid.
  • validateTree(model) / validateTreeAsync(model) still return the nested ValidationErrors<T> shape (leaves are strings, null for valid collection elements, arrays for paths that cross arrays).
const result = validator.validate(input);
if (!result.isValid) throw new ValidationException(result.errors);

const byField = result.toDictionary(); // { email: ['Email is required'] }
const codes = result.errors.map(e => e.errorCode);

ErrorTree is exported as a general type for the nested structure above; ValidationFailure, ValidationResult, and ValidationException are available as values too.

Selector and error contract

  • A selector runs once when the rule is registered, against a Proxy. During validation, values are read from the recorded path. Use a pure selector with a single property access: u => u.name, u => u.address.city, or u => u.address?.city.
  • Method calls, computations, the root object, constants, and reads of several paths are rejected. The Proxy is not a JavaScript parser: conditions that read only one path or depend on external variables cannot always be detected. Do not use branching, side effects, or changing keys in a selector.
  • A null/undefined parent yields the value undefined. Upstream string/numeric rules generally skip nullish values; add notUndefined() or notNull() if the field is required. This still applies when the field type uses a non-null assertion (!).
  • validateTree follows the upstream ValidationErrors<T> shape: nested objects; paths that cross arrays produce error arrays; numeric keys on an object stay objects. An index selector checks only that index; positions without errors are empty array slots. validate() flattens this into a ValidationFailure list with dotted/bracketed propertyName.
  • The first failing message in a chain is used by validateTree, and by validate() with cascade = 'stop'. If several registrations for the same path fail, the last error replaces the previous one, following upstream. Replacement applies to the whole result at that path, including collections. Errors from different paths are merged; a null placeholder does not erase sibling errors. A parent message wins over a child message when both fail.
  • Symbol keys and the __proto__, prototype, and constructor segments are rejected.
  • The API provides ruleFor, ruleForEach, validate, validateTree, validateAndThrow, validateAsync, validateTreeAsync, and validateAndThrowAsync. Explicit transforms are not available yet.
  • Property getters on the model are still executed when the path is read. Input objects must match the type; this library is not a schema parser for unknown data.

Development

Node.js 22.15+ is required for the process-isolation-free test runner.

npm install
npm run check

check covers type tests (including usages that must be rejected by the compiler), runtime tests, comparison of flat/collection results against upstream, the TypeScript declaration build, and installation of the tarball into a separate consumer project. The package test installs fluentv's own tarball offline into a temporary directory and asserts that fluentvalidation-ts is not installed. The package is prepared as [email protected] with public access, but has not been published. Development dependencies are only needed for building and testing.

Production usage

  • The package currently ships ESM and TypeScript declarations. The build includes source/declaration maps; npm pack runs the build automatically.
  • Define all rules before a validator is used, then reuse the instance. Do not change rules while validation is running, especially on an async validator.
  • The root input must be an object. Input coming from HTTP/JSON still has to be confirmed to match the model type before validation; TypeScript type assertions do not validate runtime data.
  • Validation failures are returned as a ValidationResult (or ValidationErrors via validateTree); validateAndThrow throws a ValidationException. Exceptions from a custom predicate, async service, or getter are still thrown; handle them at the application boundary. Timeout/cancellation for external services is the callback's responsibility.
  • Selectors are for property access only, not computation. The Proxy detection limitations above still apply.
  • Child validator results are copied before merging. The validator does not mutate the input model, but callbacks/getters written by the application can.

Local testing covers the package artifact and error-merging regressions. There is no load or long-running production proof yet; run integration tests with your application models and callbacks before rollout.

Distribution and npm release

npm install fluentv

The registry install command above applies once the package is published. To install a local build first:

npm run check
npm pack
npm install /path/to/fluentv-1.0.0.tgz

The upstream runtime snapshot, type declarations, the Apache-2.0 license, and the NOTICE attribution are included in the tarball. The build verifies the snapshot SHA-256 against src/vendor/fluentvalidation-ts/provenance.json. The engine code comes from the official v5.0.0 npm artifact and is stored unchanged; the original upstream TypeScript source is not included. Engine updates are done explicitly and re-tested through parity/regression tests.

To publish the package, use an npm account entitled to the fluentv name:

npm login
npm publish --access public

The registry name has not been verified as available. publishConfig sets public access. The package has never been published from this workspace.