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

better-effect-schema

v0.1.2

Published

Schema primitives and classes with better-result failures and better-effect composition.

Readme

better-effect-schema

Use the schema library your application already uses to validate untrusted data, construct typed values, and keep expected validation failures in better-result instead of throwing them.

better-effect-schema gives Zod, Valibot, and ArkType the same package-level model:

  • provider schemas remain native (z.object, v.object, or type({...}));
  • provider adapters connect those schemas to Schema.decode, Schema.make, and the other package operations;
  • successful values keep the provider's inferred TypeScript types; and
  • expected failures are returned as Result.err values with normalized SchemaFailure issues.

The root entrypoint is provider-neutral. Optional integrations are available from their own subpaths:

better-effect-schema       Standard Schema core and portable operations
better-effect-schema/zod   Preconfigured Zod 4 Schema facade and adapter
better-effect-schema/valibot Preconfigured Valibot Schema facade and adapter
better-effect-schema/arktype Preconfigured ArkType Schema facade and adapter

Quick start with Zod

Install the package, Zod, and better-result for inspecting operation results:

bun add better-effect-schema zod better-result

Define a real Zod schema, infer the data type you use in your application, and use the preconfigured Zod facade:

import * as z from 'zod'
import { Result } from 'better-result'
import { Schema as CoreSchema } from 'better-effect-schema'
import { Schema } from 'better-effect-schema/zod'

const UserSchema = z.object({
  id: z.string().min(1),
  email: z.email(),
  displayName: z.string().min(1)
})

type UserInput = z.infer<typeof UserSchema>

const example: UserInput = {
  id: 'user-1',
  email: '[email protected]',
  displayName: 'Ada Lovelace'
}

class User extends Schema.Class<User>('app/User')(UserSchema) {}

Validate at the boundary where data is still unknown. Schema.decodeUnknown uses the Zod schema and returns a real User instance on success:

const input: unknown = JSON.parse(
  '{"id":"user-1","email":"[email protected]","displayName":"Ada Lovelace"}'
)
const decoded = Schema.decodeUnknown(User, input)

if (Result.isError(decoded)) {
  console.error(decoded.error._tag) // SchemaDecodeFailure
  console.error(decoded.error.issues) // normalized paths and messages
  throw decoded.error
}

const user: User = decoded.value
console.log(`Welcome ${user.displayName}`)

Invalid input stays in the same explicit failure channel. You can inspect the normalized SchemaDecodeFailure returned by better-effect-schema:

const untrusted: unknown = {
  id: 42,
  email: 'not-an-email',
  displayName: ''
}

const result = Schema.decodeUnknown(User, untrusted)
if (Result.isError(result)) {
  console.error(result.error.issues)
  // result.error is a SchemaDecodeFailure; no expected validation error was thrown.
}

The validated value is now an application-level User, so pass it to your normal domain code with its exact class type:

function userLabel(user: User): string {
  return `${user.displayName} <${user.email}>`
}

if (Result.isOk(decoded)) {
  const label = userLabel(decoded.value)
  console.log(label)
}

Use Schema.decode when the input already has the schema's encoded type, and use Schema.decodeUnknown for data from JSON, HTTP, queues, or other untrusted boundaries. Both operations return Result values and can be yielded from a better-effect generator.

Choose a provider

All three providers plug into the same flow: define a native schema, validate unknown data, and hand the successful output to the package API. Choose the provider that best matches the rest of your application:

| Provider | Choose it when | Guide | | -------- | -------------------------------------------------------------------------------------------------- | -------------------------------- | | Zod | You want a broad ecosystem, codecs, or provider-owned schema classes and derivations. | Zod guide | | Valibot | You want a modular API and small bundles while keeping schemas close to ordinary data definitions. | Valibot guide | | ArkType | You prefer concise type syntax with runtime inference and detailed structural errors. | ArkType guide |

Each adapter is imported from its package subpath, so applications do not load other providers accidentally. The provider-neutral operations and failure types are documented in the API reference.

Zod codecs

When transport and application values differ, define that conversion in a Zod codec and let the package validate both directions:

const DateFromISOString = z.codec(z.iso.datetime(), z.date(), {
  decode: (value) => new Date(value),
  encode: (value) => value.toISOString()
})

class Event extends Schema.Class<Event>('app/Event')({
  id: z.string(),
  occurredAt: DateFromISOString
}) {}

const decodedEvent = Schema.decode(Event, {
  id: 'event-1',
  occurredAt: '2026-09-06T00:00:00.000Z'
})
if (Result.isError(decodedEvent)) throw decodedEvent.error

const wireEvent = CoreSchema.encode(Event, decodedEvent.value)
if (Result.isError(wireEvent)) throw wireEvent.error

Encoding is explicit. A read-only schema without a codec does not get an invented inverse encoder.

MQ integration

Use the same provider-backed schemas at a queue boundary. Define a Job with better-effect-mq and give it a schema-backed codec; Job.prepare and enqueue then validate the input before it crosses into storage. The runnable MQ codec example uses Zod 4, a Date codec, a better-effect-schema class, and a real in-memory worker.

import * as z from 'zod'
import { Codec, JobEncodeFailure, Queue } from 'better-effect-mq'
import { Schema as CoreSchema } from 'better-effect-schema'
import { Schema } from 'better-effect-schema/zod'

const DateFromISOString = z.codec(z.iso.datetime(), z.date(), {
  decode: (value) => new Date(value),
  encode: (value) => value.toISOString()
})

class SendEmailPayload extends Schema.Class<SendEmailPayload>('app/SendEmailPayload')({
  recipient: z.email(),
  subject: z.string().min(1),
  scheduledAt: DateFromISOString
}) {}

const Emails = Queue.define('emails')
const SendEmail = Emails.job('send-email', {
  version: 1,
  payload: Codec.standardSchema({
    schema: SendEmailPayload,
    encode: (value) =>
      CoreSchema.encode(SendEmailPayload, value).mapError(
        (error) => new JobEncodeFailure({ message: error.message, code: 'schema-encode' })
      )
  }),
  result: Codec.string
})

The codec accepts the schema's input shape and hands the validated output to the worker. For an in-memory value that differs from its wire value, supply an explicit encoder as shown in the runnable example.

Advanced: raw Standard Schema interoperability

Most applications should use a native provider and its adapter. A raw StandardSchemaV1 object is unusual; use it only when no provider package fits, or when you are authoring an adapter. The interoperability contract and failure behavior are documented in the advanced API section.

Tagged classes and errors

The provider-neutral root API also exports portable tagged factories. Provider subpaths add their own provider-native class capabilities where supported. Use a provider schema for ordinary input validation and the tagged factories for domain values or errors that need a stable tag:

import * as z from 'zod'
import { Result } from 'better-result'
import { Schema } from 'better-effect-schema/zod'

class UserNotFound extends Schema.TaggedError<UserNotFound>()('UserNotFound', {
  userId: z.string()
}) {}

const failure = UserNotFound.make({ userId: 'user-1' })
if (Result.isError(failure)) throw failure.error
console.log(failure.value._tag, failure.value.userId)

Package checks

The package publishes the provider-neutral root plus the ./zod, ./valibot, and ./arktype subpaths. Run the checks with Bun from the repository root or from this package:

bun run typecheck
bun run test
bun run examples
bun run check