Skip to content

Repository files navigation

Validup

Validup

A composable, path-based validation library for TypeScript.

Master WorkflowCodeQLKnown VulnerabilitiesConventional Commits

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

This monorepo publishes one core library and four integration packages:

PackageVersionDescription
validupnpmCore: Container, Validator, Issue, ValidupError
@validup/standard-schemanpmBridge to any Standard Schema library (zod, valibot, arktype, …)
@validup/zodnpmBridge to zod schemas (vendor-specific issue mapping)
@validup/validator-jsnpmPre-baked factories for validator.js (isEmail(), isLength(), …) + a generic wrap
@validup/vuenpmVue 3 form composable: vuelidate-shaped state, nested forms, async + caching

Installation

Install the core package:

npm install validup --save

Optionally 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 forms

At a Glance

import{Container,ValidupError}from'validup';import{createValidator}from'@validup/zod';import{z}from'zod';constuser=newContainer<{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{constvalid=awaituser.run({name: 'Peter',email: 'peter@example.com',});// valid is { name, email }; `age` was optional and omitted}catch(error){if(errorinstanceofValidupError){console.error(error.issues);}}

Vue Forms

The same containers drive client-side forms. @validup/vue wraps any Container in a single composable, useValidup, and returns a vuelidate-shaped state object: per-field $model / $dirty / $errors, form-level $invalid / $pending, and $validate() for submit. Pair it with @validup/zod and the schemas you already run on the server validate every keystroke in the browser:

<script setup lang="ts">import { reactive } from'vue';import { Container } from'validup';import { useValidup } from'@validup/vue';import { createValidator } from'@validup/zod';import { z } from'zod';const signup =newContainer<{ email:string; password:string }>();signup.mount('email', createValidator(z.string().email()));signup.mount('password', createValidator(z.string().min(12)));const state =reactive({ email: '', password: '' });const v =useValidup(signup, state, { debounce: 200 });asyncfunction submit() {const result =awaitv.$validate();if (result.success) {awaitsave(result.data); }}</script>
<template>
<form@submit.prevent="submit">
<inputv-model="v.fields.email.$model" />
<pv-if="v.fields.email.$errors[0]">{{ v.fields.email.$errors[0].message }}</p>
<inputv-model="v.fields.password.$model"type="password" />
<pv-if="v.fields.password.$errors[0]">{{ v.fields.password.$errors[0].message }}</p>
<button:disabled="v.$invalid || v.$pending">Sign up</button>
</form>
</template>

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, the Vue docs page, and the runnable playground/vite-vue demo (basic, groups, nested, async, server errors, and severity routes).

Why Validup

FeatureWhat it gives you
🧩 ComposableMount validators and nested Containers on any path. Stay flat or nest as deep as you like.
🌐 UniversalPure JS: runs in Node.js, browsers, Deno, Bun, and edge runtimes.
🎭 Integration-readyFirst-class bridges to Standard Schema, zod, validator.js, and Vue. Trivial to add more.
🛤️ Path-basedMount via dotted paths (a.b.c), brackets (foo[0]), or globs (**.foo).
🚦 Group-awareRun different validations for create / update / custom groups from the same container.
Optional handlingPer-mount control over undefined / null / falsy semantics.
📋 Structured errorsDiscriminated Issue items and groups with code, path, message, expected, received.
🛡️ Type-safeContainer<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-awareOpt-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.

Repository Layout

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 depends on @ebec/core (which owns the issue model), pathtrace, smob and twinop. docs/ and every playground/* workspace are private, excluded from release-please and monoship.

The Issue model (Issue, IssueItem, IssueGroup, IssueCode, the factories, guards, tree walks, prefixIssuePath, formatIssue and interpolate) is defined in @ebec/core. It lives outside this repo so other libraries can share the shape without taking on validup's runtime; issue trees then compose across libraries because both sides reference the same types.

The integration packages import those symbols directly from @ebec/core and declare it as a dependency. As of validup v2.0.0, validup no longer re-exports the model: import { IssueCode } from 'validup' no longer resolves; import from @ebec/core instead.

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.

Development

# Install workspace dependencies
npm install
# Build every package (Nx topological build)
npm run build
# Run all test suites
npm run test# Typecheck the specs (packages that opt in)
npm run test:types
# 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

Contributing

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).

License

Made with 💚

Published under Apache 2.0 License.

About

TypeScript validation library, compose validators and nested containers onto object paths, with integrations for Zod, Standard Schema, validator.js, and Vue 3.

Topics

Resources

Security policy

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages