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

@validex/core

v1.0.3

Published

Type-safe validation rules built on Zod

Readme

@validex/core

npm version npm downloads bundle size build TypeScript 5.0+ license MIT

Type-safe validation rules built on Zod — tree-shakeable, so you only ship what you use.



Why validex?

I built validex because I was fed up writing the same validation rules over and over again in every project.

Different teams, different defaults, forgetting what I had configured last time, and ending up with inconsistent behavior across the codebase. Sound familiar?

Validex was created to solve that pain once and for all:

  • One config systemsetup() lets you define your defaults globally, per-rule, or per-call. Three-tier merge (built-in defaults → global config → per-call options) so you never repeat yourself again.
  • One consistent error surfacevalidate() always returns the same clean shape — flat errors, nested errors, first-per-field, raw issues. One function, one result, every time.
  • Every error is validex-owned — no raw Zod messages leak to your users. Every error carries a namespace, code, and label for precise routing.
  • 25 production-ready rules covering the fields you actually use: identity, auth, networking, finance, and text.
  • Tree-shakeable & lightweight — 5–6 kB Brotli per rule (shared core included). All 25 rules together = 13 kB. Heavy data loads on demand.
  • i18n-ready out of the box — key mode, t() function support, label/message transforms, and a CLI that generates ready-to-translate locale files.
  • First-class framework adapters — Nuxt and Fastify integrations that feel native.

Stop copy-pasting rules. Get consistent, maintainable validation with sensible defaults — and only ship what you actually use.

Install

pnpm add @validex/core zod

Quick Start

Single rule

import { Email } from '@validex/core'

const schema = Email()
schema.parse('[email protected]') // OK
schema.parse('not-an-email')      // throws ZodError

Rule with options

import { Password } from '@validex/core'

const schema = Password({
  length: { min: 10 },
  uppercase: { min: 2 },
  blockCommon: 'basic',
})

schema.parse('ABcdefgh1!') // OK — 10+ chars, 2 uppercase, 1 digit, 1 special

Composed schema with validate()

import { z } from 'zod'
import { Email, Password, validate } from '@validex/core'

const schema = z.object({
  email: Email(),
  password: Password(),
})

const result = await validate(schema, {
  email: '[email protected]',
  password: 'Str0ng!Pass',
})

if (result.success) {
  console.log(result.data) // typed as { email: string; password: string }
} else {
  console.log(result.errors)      // { email: ['...'], password: ['...'] }
  console.log(result.firstErrors) // { email: '...', password: '...' }
}

Rules

| Rule | Description | |------|-------------| | Email | Email address with domain filtering, plus-alias blocking, and disposable detection | | Password | Strength rules: length, casing, digits, specials, consecutive limits, common-password ban | | PasswordConfirmation | Confirms two password fields match | | PersonName | Human name with unicode support, word count, and boundary rules | | BusinessName | Company/organization name with boundary and consecutive limits | | Phone | International phone via libphonenumber-js | | Website | URL restricted to http/https with optional www and domain filtering | | Url | General URL with protocol, TLD, and domain validation | | Username | Alphanumeric with configurable separators and reserved-word ban | | Slug | URL-safe slug (lowercase, hyphens, length limits) | | PostalCode | Country-aware postal/ZIP code | | LicenseKey | Software license key format (segments, separators, charset) | | Uuid | UUID v1-v7 validation | | Jwt | JSON Web Token structure with optional expiry checks | | DateTime | Date/time string with format and range constraints | | Token | Generic token validation (hex, base64, nanoid, etc.) | | Text | Free text with length, word count, content detection, and regex override | | Country | ISO 3166 country code (alpha-2, alpha-3) | | Currency | ISO 4217 currency code | | Color | Hex, RGB, HSL, and named CSS color formats | | CreditCard | Card number with Luhn check and issuer detection | | Iban | International Bank Account Number with country patterns | | VatNumber | EU VAT identification number | | MacAddress | MAC address (colon, hyphen, and dot notations) | | IpAddress | IPv4 and IPv6 with optional CIDR notation |

Bundle Size

Every rule shares a ~5 kB core (Brotli). Each additional rule adds 0.1-0.8 kB. Measured with esbuild --splitting + Brotli, excluding zod peer dependency.

| Rule | Initial (Brotli) | On-demand data | Trigger | |------|-----------------|----------------|---------| | Email | 5.7 kB | — | — | | Password | 5.6 kB | +0.5 kB / +3.8 kB / +35.5 kB | blockCommon: 'basic' / 'moderate' / 'strict' | | PasswordConfirmation | 5.7 kB | — | — | | PersonName | 5.7 kB | — | — | | BusinessName | 5.7 kB | — | — | | Phone | 5.7 kB | external | libphonenumber-js (bundled) | | Website | 5.7 kB | — | — | | Url | 5.6 kB | — | — | | Username | 5.9 kB | +0.8 kB | blockReserved: true | | Slug | 5.5 kB | — | — | | PostalCode | 5.4 kB | external | postcode-validator (bundled) | | LicenseKey | 5.5 kB | — | — | | Uuid | 5.3 kB | — | — | | Jwt | 5.6 kB | — | — | | DateTime | 5.6 kB | — | — | | Token | 5.5 kB | — | — | | Text | 5.5 kB | — | — | | Country | 5.4 kB | +2.4 kB | First use | | Currency | 5.4 kB | +0.3 kB | First use | | Color | 5.5 kB | — | — | | CreditCard | 5.6 kB | +0.3 kB | First use | | Iban | 5.5 kB | +0.7 kB | First use | | VatNumber | 5.5 kB | +0.3 kB | First use | | MacAddress | 5.3 kB | — | — | | IpAddress | 5.6 kB | — | — |

| Combination | Initial (Brotli) | |-------------|-----------------| | Email + Password | 6.0 kB | | Form (Email + Password + PersonName + Phone) | 6.9 kB | | All 25 rules | 13.0 kB |

"On-demand data" loads asynchronously on first use or when the listed option is enabled. Not included in the initial bundle.

Configuration

Global defaults with setup()

import { setup, Email, Password } from '@validex/core'

setup({
  rules: {
    email: { blockDisposable: true },
    password: { length: { min: 10 }, special: { min: 2 } },
  },
  i18n: {
    enabled: true,
    t: (key, params) => translate(key, params),
  },
})

// Rules now use your defaults — no need to pass options every time
const emailSchema = Email()
const passwordSchema = Password()

Three-tier merge

built-in defaults  <  setup() config  <  per-call options

Per-call options override setup() config, which overrides built-in defaults. Passing undefined for a per-call option removes the global setting for that field.

import { setup, Email } from '@validex/core'

setup({ rules: { email: { blockDisposable: true } } })

Email()                              // blockDisposable: true (from setup)
Email({ blockPlusAlias: true })      // blockDisposable: true + blockPlusAlias: true
Email({ blockDisposable: undefined }) // blockDisposable removed for this call

resetConfig()

import { resetConfig } from '@validex/core'

resetConfig() // resets to built-in defaults

preloadData()

Preload async data files at startup so first validation has no delay:

import { preloadData } from '@validex/core'

await preloadData({
  disposable: true,
  passwords: 'moderate',
  reserved: true,
  phone: 'mobile',
  countryCodes: true,
  currencyCodes: true,
  ibanPatterns: true,
  vatPatterns: true,
  creditCardPrefixes: true,
  postalCodes: true,
})

Cross-Field Validation

sameAs

Creates a superRefine callback that verifies two fields hold the same value:

import { z } from 'zod'
import { Password, sameAs } from '@validex/core'

const schema = z.object({
  password: Password(),
  confirmPassword: z.string(),
}).superRefine(sameAs('confirmPassword', 'password', {
  message: 'Passwords do not match',
}))

PasswordConfirmation auto-wires this — it registers a sameAs: 'password' constraint automatically:

import { z } from 'zod'
import { Password, PasswordConfirmation, validate } from '@validex/core'

const schema = z.object({
  password: Password(),
  confirmPassword: PasswordConfirmation(),
})

const result = await validate(schema, {
  password: 'Str0ng!Pass',
  confirmPassword: 'different',
})
// result.firstErrors.confirmPassword → "Password Confirmation must match Password"

requiredWhen

Creates a superRefine callback that marks a field as required when a condition is met:

import { z } from 'zod'
import { requiredWhen } from '@validex/core'

const schema = z.object({
  accountType: z.string(),
  companyName: z.string().optional(),
}).superRefine(requiredWhen(
  'companyName',
  (data) => data['accountType'] === 'business',
  { message: 'Company name is required for business accounts' },
))

validate() resolves cross-field

schema.safeParse() only runs field-level validation. validate() adds cross-field checks (sameAs, requiredWhen) after Zod parsing:

// safeParse — field-level only, no cross-field
const zodResult = schema.safeParse(data)

// validate — runs field-level + cross-field
const result = await validate(schema, data)

Chainable Methods

Import @validex/core and all Zod schemas get these methods (intended for use on string schemas):

Checks (return same type, add refinement)

| Method | Options | Description | |--------|---------|-------------| | .hasUppercase(opts?) | min?, max? | Requires uppercase letters | | .hasLowercase(opts?) | min?, max? | Requires lowercase letters | | .hasDigits(opts?) | min?, max? | Requires digits | | .hasSpecial(opts?) | min?, max? | Requires special characters | | .noEmails(opts?) | — | Blocks email addresses | | .noUrls(opts?) | — | Blocks URLs | | .noHtml(opts?) | — | Blocks HTML tags | | .noPhoneNumbers(opts?) | — | Blocks phone numbers | | .noSpaces(opts?) | — | Blocks whitespace | | .onlyAlpha(opts?) | — | Letters only | | .onlyNumeric(opts?) | — | Digits only | | .onlyAlphanumeric(opts?) | — | Letters + digits | | .onlyAlphaSpaceHyphen(opts?) | — | Letters, spaces, hyphens | | .onlyAlphanumericSpaceHyphen(opts?) | — | Letters, digits, spaces, hyphens | | .maxWords(opts) | max | Maximum word count | | .minWords(opts) | min | Minimum word count | | .maxConsecutive(opts) | max | Max consecutive identical chars |

Transforms (return ZodPipe)

| Method | Description | |--------|-------------| | .toTitleCase() | Converts to Title Case | | .toSlug() | Converts to url-safe-slug | | .stripHtml() | Removes HTML tags | | .collapseWhitespace() | Collapses multiple spaces to single | | .emptyToUndefined() | Converts "" to undefined |

import { z } from 'zod'
import '@validex/core'

const schema = z.string().hasUppercase({ min: 2 }).noSpaces().toSlug()

Check Functions

Pure functions, no Zod dependency. Import from @validex/core/checks.

Composition

| Function | Signature | Description | |----------|-----------|-------------| | hasUppercase | (value: string, min: number, max?: number) => boolean | Uppercase letter count within [min, max] | | hasLowercase | (value: string, min: number, max?: number) => boolean | Lowercase letter count within [min, max] | | hasDigits | (value: string, min: number, max?: number) => boolean | Digit count within [min, max] | | hasSpecial | (value: string, min: number, max?: number) => boolean | Special character count within [min, max] |

Detection

| Function | Signature | Description | |----------|-----------|-------------| | containsEmail | (value: string) => boolean | Detects email-like patterns | | containsUrl | (value: string) => boolean | Detects URL-like patterns | | containsHtml | (value: string) => boolean | Detects HTML tags | | containsPhoneNumber | (value: string) => Promise<boolean> | Detects phone numbers (async, uses libphonenumber-js) |

Restriction

| Function | Signature | Description | |----------|-----------|-------------| | onlyAlpha | (value: string) => boolean | Every character is a unicode letter | | onlyNumeric | (value: string) => boolean | Every character is a digit | | onlyAlphanumeric | (value: string) => boolean | Every character is a letter or digit | | onlyAlphaSpaceHyphen | (value: string) => boolean | Letters, spaces, hyphens only | | onlyAlphanumericSpaceHyphen | (value: string) => boolean | Letters, digits, spaces, hyphens only |

Limits

| Function | Signature | Description | |----------|-----------|-------------| | maxWords | (value: string, max: number) => boolean | At most max words | | minWords | (value: string, min: number) => boolean | At least min words | | maxConsecutive | (value: string, max: number) => boolean | No character repeats more than max times | | noSpaces | (value: string) => boolean | No whitespace characters |

Transforms

| Function | Signature | Description | |----------|-----------|-------------| | emptyToUndefined | (value: unknown) => unknown | "" and null to undefined | | toTitleCase | (value: string) => string | Title Case with hyphen/apostrophe handling | | toSlug | (value: string) => string | URL-safe slug | | stripHtml | (value: string) => string | Removes HTML tags | | collapseWhitespace | (value: string) => string | Collapses whitespace, trims |

import { hasUppercase, containsEmail, toSlug } from '@validex/core/checks'

hasUppercase('Hello', 1)    // true
containsEmail('[email protected]') // true
toSlug('Hello World!')       // 'hello-world'

Error Handling

Error structure

Every validex error carries structured metadata via Zod's custom error params:

ctx.addIssue({
  code: 'custom',
  params: {
    code: 'disposableBlocked',
    namespace: 'email',
    label: 'Email',
    domain: 'tempmail.com',
  },
})

getParams(issue)

Extract structured metadata from any Zod issue:

import { Email, getParams } from '@validex/core'

const schema = Email()
const result = schema.safeParse('[email protected]')

if (!result.success) {
  const params = getParams(result.error.issues[0])
  // { code: 'disposableBlocked', namespace: 'email', label: 'Email',
  //   key: 'validation.messages.email.disposableBlocked', path: [], ... }
}

Error code pattern

Keys follow: validation.messages.{namespace}.{code}

  • validation.messages.email.disposableBlocked
  • validation.messages.password.commonBlocked
  • validation.messages.username.reservedBlocked

validate() result

interface ValidationResult<T> {
  readonly success: boolean
  readonly data?: T                                    // typed parsed data (when success)
  readonly errors: Record<string, readonly string[]>   // dot-path to all messages
  readonly firstErrors: Record<string, string>         // dot-path to first message
  readonly nestedErrors: NestedErrors                  // nested object matching schema shape
  readonly issues: ReadonlyArray<unknown>              // raw Zod issues (escape hatch)
}

i18n

Setup

import { setup } from '@validex/core'

setup({
  i18n: {
    enabled: true,
    prefix: 'validation',    // default
    separator: '.',          // default
    t: (key, params) => i18next.t(key, params),
  },
})

Key pattern

validation.messages.{namespace}.{code}

When i18n.enabled is true and t() is provided, validex calls t() automatically for every error message and field label.

Label transforms

setup({
  label: {
    fallback: 'derived',  // 'derived' | 'generic' | 'none'
    transform: ({ path, fieldName, defaultLabel }) => {
      return myLabelLookup(fieldName) ?? defaultLabel
    },
  },
})

CLI

npx validex fr de --output ./locales
npx validex ja --empty --output ./locales

Full guide with all 141 error codes: Translation Guide

Custom Rules

createRule()

import { createRule } from '@validex/core'
import { z } from 'zod'

interface HexColorOptions {
  label?: string
  emptyToUndefined?: boolean
  normalize?: boolean
  customFn?: (value: string) => true | string | Promise<true | string>
  allowAlpha?: boolean
}

const HexColor = createRule<HexColorOptions>({
  name: 'hexColor',
  defaults: { allowAlpha: false },
  build: (opts) => {
    const pattern = opts.allowAlpha
      ? /^#[\da-f]{6,8}$/i
      : /^#[\da-f]{6}$/i
    return z.string().regex(pattern)
  },
  messages: {
    invalid: '{{label}} is not a valid hex color',
  },
})

const schema = HexColor({ allowAlpha: true })
schema.parse('#ff00aacc') // OK

customFn

Every rule accepts a customFn that runs after built-in checks. Return true to pass or a string to fail:

import { Email } from '@validex/core'

const schema = Email({
  customFn: (value) => value.endsWith('.org') || 'Must be a .org domain',
})

schema.parse('[email protected]')  // OK
schema.parse('[email protected]')  // throws — "Must be a .org domain"

Custom regex

Rules that extend FormatRuleOptions (like Text) accept a regex property:

import { Text } from '@validex/core'

const schema = Text({
  regex: /^[^<>]+$/,
})

Full reference: API Reference

Framework Adapters

  • @validex/nuxt — Nuxt module with auto-imports and useValidation composable
  • @validex/fastify — Fastify plugin with request validation decorators

License

MIT