A composable, path-based validation library for TypeScript.
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.
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
This monorepo publishes one core library and four integration packages:
| Package | Version | Description |
|---|---|---|
validup |
Core: Container, Validator, Issue, ValidupError |
|
@validup/standard-schema |
Bridge to any Standard Schema library (zod, valibot, arktype, …) | |
@validup/zod |
Bridge to zod schemas (vendor-specific issue mapping) | |
@validup/validator-js |
Pre-baked factories for validator.js (isEmail(), isLength(), …) + a generic wrap |
|
@validup/vue |
Vue 3 composable for client-side forms |
Install the core package:
npm install validup --saveOptionally add an integration:
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 formsimport { 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);
}
}| Feature | What it gives you |
|---|---|
| 🧩 Composable | Mount validators and nested Containers 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<T> propagates the output shape; mount paths are checked against T. The opt-in defineSchema() builder accumulates T from the registered mounts so run()'s static return type matches what was registered. |
| ⚡ Cache-aware | Opt-in per-mount result cache ({ cache: new ResultCache() }) — defineValidator({ sideEffect: true }) opts the rare cross-field / async validator out so per-keystroke runs reuse fresh results without re-firing slow checks. |
validup/
├── packages/
│ ├── validup/ # Core library
│ ├── standard-schema/ # @validup/standard-schema
│ ├── zod/ # @validup/zod
│ ├── validator-js/ # @validup/validator-js
│ └── vue/ # @validup/vue
├── docs/ # VitePress site (private)
├── playground/
│ └── vite-vue/ # Vite + Vue 3 demo app (private, multi-route)
├── nx.json # Nx caching for build / lint / test
└── release-please-config.json
The five packages are managed as an Nx workspace under npm workspaces. Integration packages depend on validup; the core has a single runtime dep on @ebec/core. docs/ and every playground/* workspace are private — excluded from release-please and monoship.
The Vite + Vue playground lives at playground/vite-vue and exercises @validup/vue end-to-end (basic form, groups, nested forms, async + debounce, server errors, severity). Run it with npm run dev --workspace=@validup-playground/vite-vue.
# Install workspace dependencies
npm install
# Build every package (Nx topological build)
npm run build
# Run all test suites
npm run test
# Lint
npm run lint
npm run lint:fix- Node.js:
>=24.0.0(CI runs on 24) - Test runner: Vitest 4
- Bundler: tsdown — ESM-only output (
dist/index.mjs+dist/index.d.mts) - Lint: ESLint v10 flat config
- Releases: managed by release-please — one component per package; published via
tada5hi/monoship
Issues and pull requests are welcome. Please follow Conventional Commits — commit messages are linted via commitlint. CI runs install → build → lint → test on every PR.
For security vulnerabilities, please email contact@tada5hi.net rather than opening a public issue (see SECURITY.md).
Made with 💚
Published under Apache 2.0 License.