A composable, path-based validation library for TypeScript.
[![Master Workflow][workflow-src]][workflow-href] [![CodeQL][codeql-src]][codeql-href] [![Known Vulnerabilities][snyk-src]][snyk-href] [![Conventional Commits][conventional-src]][conventional-href] Mount validators and nested containers onto object paths, run them in groups, collect structured issues, and bridge to your favorite validator (zod, validator.js) or framework (Vue), all without decorators or schema DSLs. ## Core Philosophy Most validation libraries make you choose between two extremes: a schema DSL that bakes in every rule, or a hand-rolled function that tangles parsing, validation, and transformation. Validup picks a different point: **mount any validator function onto any path of any input, compose containers, and let the runtime handle path expansion, error aggregation, and group filtering.** You bring the validators (or wrap an existing library via an integration package); validup orchestrates them. **Table of Contents** - [Packages](#packages) - [Installation](#installation) - [At a Glance](#at-a-glance) - [Vue Forms](#vue-forms) - [Why Validup](#why-validup) - [Repository Layout](#repository-layout) - [Development](#development) - [Contributing](#contributing) - [License](#license) ## Packages This monorepo publishes one core library and four integration packages: | Package | Version | Description | |--------------------------------------------------------------|-----------------------------------------------|--------------------------------------------------------------------------| | [`validup`](./packages/validup) | [![npm][validup-npm-src]][validup-npm-href] | Core: `Container`, `Validator`, `Issue`, `ValidupError` | | [`@validup/standard-schema`](./packages/standard-schema) | [![npm][ss-npm-src]][ss-npm-href] | Bridge to any [Standard Schema](https://standardschema.dev) library (zod, valibot, arktype, …) | | [`@validup/zod`](./packages/zod) | [![npm][zod-npm-src]][zod-npm-href] | Bridge to [zod](https://zod.dev) schemas (vendor-specific issue mapping) | | [`@validup/validator-js`](./packages/validator-js) | [![npm][vjs-npm-src]][vjs-npm-href] | Pre-baked factories for [validator.js](https://github.com/validatorjs/validator.js) (`isEmail()`, `isLength()`, …) + a generic wrap | | [`@validup/vue`](./packages/vue) | [![npm][vue-npm-src]][vue-npm-href] | [Vue 3](https://vuejs.org) form composable: vuelidate-shaped state, nested forms, async + caching | ## Installation Install the core package: ```bash npm install validup --save ``` Optionally add an integration: ```bash npm install @validup/standard-schema --save # Standard Schema (zod 3.24+, valibot, arktype, …) npm install @validup/zod --save # zod-specific (richer issue mapping) npm install @validup/validator-js validator --save # validator.js string validators npm install @validup/vue --save # Vue 3 forms ``` ## At a Glance ```typescript import { Container, ValidupError } from 'validup'; import { createValidator } from '@validup/zod'; import { z } from 'zod'; const user = new Container<{ name: string; email: string; age: number }>(); user.mount('name', createValidator(z.string().min(2))); user.mount('email', createValidator(z.string().email())); user.mount('age', { optional: true }, createValidator(z.number().int().positive())); try { const valid = await user.run({ name: 'Peter', email: 'peter@example.com', }); // valid is { name, email }; `age` was optional and omitted } catch (error) { if (error instanceof ValidupError) { console.error(error.issues); } } ``` ## Vue Forms The same containers drive client-side forms. [`@validup/vue`](./packages/vue) wraps any `Container` in a single composable, `useValidup`, and returns a [vuelidate](https://vuelidate-next.netlify.app/)-shaped state object: per-field `$model` / `$dirty` / `$errors`, form-level `$invalid` / `$pending`, and `$validate()` for submit. Pair it with [`@validup/zod`](./packages/zod) and the schemas you already run on the server validate every keystroke in the browser: ```vue ``` What you get beyond the basics: - **Async-aware**: every scheduled run owns an `AbortController`, so keystrokes cancel stale runs and `debounce` collapses bursts; `$validate()` deliberately runs without a signal so submit can't be aborted mid-flight. - **Result caching**: a per-form `ResultCache` replays unchanged fields, so editing `name` never re-fires the async uniqueness check on `email`. - **Nested forms**: child components register with their ancestor form via `provide` / `inject`; the parent reads them reactively through `$getResultsForChild` for live aggregation. - **Server errors**: feed a `ValidupError` from your API back with `setExternalIssues()`; the issues land on the matching fields and clear as the user retypes. - **Severity**: `getSeverity(field)` renders optional-mount failures as warnings instead of errors, so pristine forms stay friendly. See the [`@validup/vue` README](./packages/vue), the [Vue docs page](https://validup.tada5hi.net/integrations/vue), and the runnable [`playground/vite-vue`](./playground/vite-vue) demo (basic, groups, nested, async, server errors, and severity routes). ## Why Validup | Feature | What it gives you | |--------------------------|----------------------------------------------------------------------------------------------| | 🧩 **Composable** | Mount validators and nested `Container`s on any path. Stay flat or nest as deep as you like. | | 🌐 **Universal** | Pure JS: runs in Node.js, browsers, Deno, Bun, and edge runtimes. | | 🎭 **Integration-ready** | First-class bridges to Standard Schema, zod, validator.js, and Vue. Trivial to add more. | | 🛤️ **Path-based** | Mount via dotted paths (`a.b.c`), brackets (`foo[0]`), or globs (`**.foo`). | | 🚦 **Group-aware** | Run different validations for `create` / `update` / custom groups from the same container. | | ❓ **Optional handling** | Per-mount control over `undefined` / `null` / falsy semantics. | | 📋 **Structured errors** | Discriminated `Issue` items and groups with code, path, message, expected, received. | | 🛡️ **Type-safe** | `Container