Skip to content

validup

Composable, path-based validation for TypeScript. Mount validators on any path, run them in groups, and collect structured issues — without decorators or schema DSLs.

Apache 2.0 licensed · Node 22+ · ESM-only · TypeScript-first

signup.safeRunSync(state)
✗ 2 issueslive
{
  "success": false,
  "issues": [
    {
      "type": "item",
      "path": [
        "email"
      ],
      "message": "Must look like an email address",
      "code": "value_invalid"
    },
    {
      "type": "item",
      "path": [
        "password"
      ],
      "message": "Must be at least 8 characters",
      "code": "value_invalid"
    }
  ]
}

Each keystroke re-runs the Container and re-renders the structured Issue[] — same model that powers integrations with zod, express-validator, Routup, and Vue.

One model. Six superpowers.

A small core (Container · Validator · Issue) — and everything else falls out from how you compose it.

🧩

Composable

Mount validators and nested Containers on any path. Stay flat or nest as deep as the input demands.

🛤️

Path-based

Mount via dotted paths (a.b.c), brackets (foo[0]), or globs (**.foo) — expansion is handled by pathtrace.

🚦

Group-aware

Run different validations for create / update / custom groups from the same container — no schema duplication.

📋

Structured issues

Discriminated Issue items and groups with code, path, message, expected, received — ready for any UI.

🌐

Universal

Pure JS, ESM-only. Runs in Node, browsers, Deno, Bun, and edge runtimes — no Node-specific deps.

🛡️

Type-safe

Container<T, C> propagates output shape and a typed user context all the way through nested mounts.

From zero to first validation

Three steps. No decorators, no schema DSL, no build-time codegen.

npm install validup

@validup/vue

Reactive forms, vuelidate-shaped

A single composable turns any Container into reactive form state. Errors gate on dirty paths, debounce on keystrokes, and abort cleanly when the user keeps typing.

  • $invalid · $dirty · $errors — vuelidate-style API, no schema lock-in
  • $model — two-way bound, dirty-tracked, deep-path safe
  • Debounce + abort — stale runs cancel automatically on new input
  • Nested forms — scope composables, aggregate child results from a parent
Read the Vue guide →
SignupForm.vue
// SignupForm.vue
import { Container } from 'validup';
import { useValidup } from '@validup/vue';
import { reactive } from 'vue';

const signup = new Container<{ email: string; password: string }>();
signup.mount('email', isEmail);
signup.mount('password', isStrongPassword);

const state = reactive({ email: '', password: '' });
const v = useValidup(signup, state, { debounce: 200 });

async function submit() {
    const result = await v.$validate();
    if (result.success) save(result.data);
}

Released under the Apache 2.0 License.