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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@sylphx/vex

v0.1.11

Published

⚡ Ultra-fast schema validation - 5x faster than Valibot, 30x faster than Zod

Readme

@sylphx/vex

⚡ Ultra-fast schema validation - 12x faster than Zod, 6x faster than Valibot

Installation

npm install @sylphx/vex
# or
bun add @sylphx/vex
# or
pnpm add @sylphx/vex

Basic Usage

import { str, num, object, array, email, int, positive, min, max, safeParse } from '@sylphx/vex'

// Create a schema
const userSchema = object({
  name: str(min(1), max(100)),
  email: str(email),
  age: num(int, positive),
})

// Validate data (throws on error)
const user = userSchema({ name: 'Alice', email: '[email protected]', age: 30 })

// Or use safeParse (returns result object)
const result = safeParse(userSchema)({ name: 'Bob', email: 'invalid', age: -1 })
if (result.success) {
  console.log(result.data)
} else {
  console.log(result.error) // "email: Invalid email"
}

Core Concepts

1. Type Validators

Vex provides composable type validators:

import { str, num, bool, arr, obj } from '@sylphx/vex'

// Base validators (no constraints)
str()('hello')      // ✅ 'hello'
str()(123)          // ❌ throws "Expected string"

num()(42)           // ✅ 42
num()('42')         // ❌ throws "Expected number"

bool()(true)        // ✅ true

2. Adding Constraints

Pass constraints directly to type validators:

import { str, num, email, url, int, positive, min, max, gte, lte } from '@sylphx/vex'

// String with constraints
str(email)                    // string + email format
str(url)                      // string + URL format
str(min(3), max(20))          // string + length between 3-20

// Number with constraints
num(int)                      // integer
num(positive)                 // positive number
num(int, positive)            // positive integer
num(gte(0), lte(100))         // number between 0-100

// Usage
str(email)('[email protected]')    // ✅ '[email protected]'
str(email)('invalid')             // ❌ throws "Invalid email"

num(int, positive)(25)            // ✅ 25
num(int, positive)(-5)            // ❌ throws "Must be positive"

3. Object Schemas

import { object, str, num, email, int, positive, optional, min } from '@sylphx/vex'

const userSchema = object({
  id: str(),
  name: str(min(1)),
  email: str(email),
  age: num(int, positive),
  bio: optional(str()),  // optional field
})

// TypeScript infers the type automatically
type User = ReturnType<typeof userSchema>
// { id: string; name: string; email: string; age: number; bio?: string }

const user = userSchema({
  id: '123',
  name: 'Alice',
  email: '[email protected]',
  age: 30,
})

4. Array Schemas

import { array, str, num, email, int, nonempty } from '@sylphx/vex'

const emailsSchema = array(str(email))
const scoresSchema = array(num(int))
const tagsSchema = array(str(nonempty))

emailsSchema(['[email protected]', '[email protected]'])  // ✅
scoresSchema([1, 2, 3])               // ✅
tagsSchema(['a', '', 'c'])            // ❌ throws "[1]: Required"

5. Error Handling

Three ways to handle validation errors:

import { str, email, safeParse, tryParse, ValidationError } from '@sylphx/vex'

const schema = str(email)

// Method 1: Direct call (throws on error)
try {
  schema('invalid')
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.message) // "Invalid email"
  }
}

// Method 2: safeParse (returns result object)
const result = safeParse(schema)('invalid')
if (!result.success) {
  console.log(result.error) // "Invalid email"
}

// Method 3: tryParse (returns null on error)
const value = tryParse(schema)('invalid') // null

Common Patterns

Form Validation

import { object, str, num, email, min, max, int, positive, safeParse } from '@sylphx/vex'

const signupSchema = object({
  username: str(min(3), max(20)),
  email: str(email),
  password: str(min(8)),
  age: num(int, positive),
})

function handleSubmit(formData: unknown) {
  const result = safeParse(signupSchema)(formData)

  if (!result.success) {
    return { error: result.error }
  }

  // result.data is fully typed
  return { user: result.data }
}

API Response Validation

import { object, array, str, num, bool, int, positive } from '@sylphx/vex'

const apiResponseSchema = object({
  status: str(),
  data: array(object({
    id: num(int, positive),
    title: str(),
    completed: bool(),
  })),
})

async function fetchTodos() {
  const response = await fetch('/api/todos')
  const json = await response.json()

  // Throws if response doesn't match schema
  return apiResponseSchema(json)
}

Optional & Nullable

import { str, email, optional, nullable, nullish, withDefault } from '@sylphx/vex'

// optional - allows undefined
const optionalEmail = optional(str(email))
optionalEmail(undefined)           // ✅ undefined
optionalEmail('[email protected]')  // ✅ '[email protected]'

// nullable - allows null
const nullableEmail = nullable(str(email))
nullableEmail(null)                // ✅ null

// nullish - allows null or undefined
const nullishEmail = nullish(str(email))

// withDefault - provides default value
const emailWithDefault = withDefault(str(email), '[email protected]')
emailWithDefault(undefined)        // ✅ '[email protected]'

Union Types

import { union, str, num, literal } from '@sylphx/vex'

// Union of types (variadic syntax)
const stringOrNumber = union(str(), num())
stringOrNumber('hello')  // ✅ 'hello'
stringOrNumber(42)       // ✅ 42
stringOrNumber(true)     // ❌ throws

// Literal union (discriminated)
const status = union(literal('pending'), literal('active'), literal('done'))
status('active')         // ✅ 'active'
status('invalid')        // ❌ throws

Transforms

import { str, num, trim, lower, upper, toInt, toFloat, toDate, pipe } from '@sylphx/vex'

// String transforms (use pipe for transforms)
const normalizedEmail = pipe(str(), trim, lower)
normalizedEmail('  [email protected]  ')  // '[email protected]'

// Parse string to number
const parseAge = pipe(str(), toInt)
parseAge('42')  // 42

// Parse string to Date
const parseDate = pipe(str(), toDate)
parseDate('2024-01-15')  // Date object

API Reference

Type Validators

| Validator | Description | |-----------|-------------| | str(...constraints) | String validator | | num(...constraints) | Number validator (excludes NaN) | | bool() | Boolean validator | | arr() | Array validator | | obj() | Object validator | | bigInt() | BigInt validator | | date() | Date validator |

String Constraints

| Constraint | Description | |------------|-------------| | email | Email format | | url | URL format | | uuid | UUID format | | min(n) | Minimum length | | max(n) | Maximum length | | len(n) | Exact length | | nonempty | Non-empty string | | pattern(regex) | Regex pattern | | startsWith(s) | Starts with string | | endsWith(s) | Ends with string | | includes(s) | Contains string |

Number Constraints

| Constraint | Description | |------------|-------------| | int | Integer | | positive | > 0 | | negative | < 0 | | finite | Finite number | | gte(n) | >= n | | lte(n) | <= n | | gt(n) | > n | | lt(n) | < n | | multipleOf(n) | Divisible by n |

Transforms

| Transform | Description | |-----------|-------------| | trim | Trim whitespace | | lower | Lowercase | | upper | Uppercase | | toInt | Parse to integer | | toFloat | Parse to float | | toDate | Parse to Date |

Composition

| Function | Description | |----------|-------------| | object(shape, ...meta) | Object schema | | array(schema, ...meta) | Array schema | | tuple(...schemas, ...meta) | Tuple schema | | union(...schemas, ...meta) | Union type | | intersect(...schemas, ...meta) | Intersection type | | optional(schema) | Allow undefined | | nullable(schema) | Allow null | | nullish(schema) | Allow null or undefined | | withDefault(schema, value) | Default value | | pipe(...validators) | Chain validators (for transforms) |

Metadata

Metadata modifiers return MetaAction and are passed directly to schema functions:

import { str, num, object, union, description, title, examples } from '@sylphx/vex'

// Add metadata to primitives
str(description('User name'))
str(min(1), description('Required name'), title('Name'))
num(int, positive, examples([1, 2, 3]))

// Add metadata to composites
union(str(), num(), description('String or number'))
object({ name: str() }, description('User object'))
array(str(), description('List of names'))
tuple(str(), num(), description('Name and age pair'))

| Function | Description | |----------|-------------| | description(text) | Add description (returns MetaAction) | | title(text) | Add title (returns MetaAction) | | examples(values) | Add examples (returns MetaAction) | | deprecated() | Mark as deprecated (returns MetaAction) | | metadata(obj) | Add multiple metadata fields (returns MetaAction) | | brand(schema, name) | Nominal typing (strict) - wraps schema | | flavor(schema, name) | Nominal typing (weak) - wraps schema | | readonly(schema) | Mark as readonly - wraps schema | | getDescription(schema) | Get description | | getTitle(schema) | Get title | | getExamples(schema) | Get examples | | getMeta(schema) | Get all metadata |

Utilities

| Function | Description | |----------|-------------| | safeParse(schema)(data) | Returns { success, data/error } | | tryParse(schema)(data) | Returns data or null | | toJsonSchema(schema) | Convert to JSON Schema |

Standard Schema

Vex implements Standard Schema v1, compatible with:

  • tRPC
  • TanStack Form/Router
  • Hono
  • Remix
  • And more...
// All validators expose ~standard property
const schema = object({ email: str(email) })
schema['~standard'].validate(data)

JSON Schema

Convert Vex schemas to JSON Schema (draft-07, draft-2019-09, draft-2020-12):

import { object, str, num, email, int, positive, optional, toJsonSchema } from '@sylphx/vex'

const userSchema = object({
  name: str(),
  email: str(email),
  age: num(int, positive),
  bio: optional(str()),
})

// Convert to JSON Schema
const jsonSchema = toJsonSchema(userSchema)
// {
//   "$schema": "http://json-schema.org/draft-07/schema#",
//   "type": "object",
//   "properties": {
//     "name": { "type": "string" },
//     "email": { "type": "string", "format": "email" },
//     "age": { "type": "integer", "exclusiveMinimum": 0 },
//     "bio": { "type": "string" }
//   },
//   "required": ["name", "email", "age"]
// }

Options

// Choose JSON Schema draft version
toJsonSchema(schema, { draft: 'draft-2020-12' })

// Without $schema property
toJsonSchema(schema, { $schema: false })

// With named definitions
toJsonSchema(schema, {
  definitions: {
    User: userSchema,
    Post: postSchema,
  }
})

Use Cases

  • OpenAPI/Swagger: Generate API documentation
  • Form builders: Auto-generate forms from schemas
  • Code generation: Generate types for other languages
  • Validation interop: Share schemas with non-JS systems

Schema Metadata

Add documentation metadata to your schemas using MetaAction modifiers:

import { str, num, object, union, description, title, examples, toJsonSchema, getMeta } from '@sylphx/vex'

// Add metadata directly in schema definitions
const emailSchema = str(
  email,
  title('Email'),
  description('User email address'),
  examples(['[email protected]', '[email protected]'])
)

// Works with all schema types
const userSchema = object(
  {
    name: str(min(1), description('User full name')),
    email: emailSchema,
    age: num(int, positive, description('User age in years')),
  },
  description('User account object'),
  title('User')
)

// Works with unions too
const idSchema = union(
  str(),
  num(),
  description('Identifier can be string or number')
)

// Metadata flows to JSON Schema
toJsonSchema(emailSchema)
// {
//   "type": "string",
//   "format": "email",
//   "title": "Email",
//   "description": "User email address",
//   "examples": ["[email protected]", "[email protected]"]
// }

Nominal Typing

Use brand for strict nominal types or flavor for weak nominal types:

import { str, uuid, brand, flavor, pipe } from '@sylphx/vex'

// Strict brand - types are incompatible
type UserId = string & { __brand: 'UserId' }
const userId = brand(pipe(str(), uuid), 'UserId')

// Weak flavor - types are compatible but distinguishable
type Email = string & { __flavor?: 'Email' }
const emailFlavor = flavor(str(email), 'Email')

Performance

| Benchmark | Vex | Zod | Valibot | vs Zod | vs Valibot | |-----------|-----|-----|---------|--------|------------| | string | 367M | 50M | 50M | 7.4x | 7.3x | | email | 128M | 12M | 17M | 11x | 7.5x | | url | 110M | 4M | 5M | 28x | 23x | | uuid | 117M | 16M | 6M | 7.3x | 19x | | object (3 fields) | 18M | 5M | 7M | 3.9x | 2.7x | | object (nested) | 11M | 5M | 5M | 2x | 2.1x | | array[50] | 8.5M | 780K | 1.4M | 11x | 6x | | safeParse (valid) | 15M | 7M | 6M | 2.1x | 2.5x | | safeParse (invalid) | 44M | 376K | 2.6M | 118x | 17x |

Average: 12x faster than Zod, 6x faster than Valibot

License

MIT