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

@thomasfosterau/effect-forms

v0.1.0

Published

Framework-neutral, Effect-native form validation and state — schema-driven validation, nested errors, posted-value retention, and native constraints, on Effect Schema.

Readme

@thomasfosterau/effect-forms

Framework-neutral, Effect-native form validation and state — schema-driven validation, nested errors, posted-value retention, status messages, and native input constraints, built on Effect Schema with no framework dependency. The Svelte 5 bindings live in @thomasfosterau/effect-forms-svelte.

Why

The app validates everything with Effect Schema (Zod is gone). The progressive form libraries in the ecosystem are excellent, but they speak Standard Schema / Zod and bake in a framework. This package keeps the ergonomics — one schema drives validation, errors nest by field, posted values survive a failed submit, native input constraints come for free — while staying on Effect Schema and free of any framework, so the core is testable in isolation and reusable from a Worker, the CLI, or a Svelte route.

The shape

import * as Schema from "effect/Schema";
import * as Effect from "effect/Effect";
import { Form } from "@thomasfosterau/effect-forms";

const Signup = Schema.Struct({
  name: Schema.String.check(Schema.isMinLength(2)),
  age: Schema.optional(Schema.FiniteFromString),
});

const signupForm = Form.make(Signup, {
  id: "signup",
  defaults: { name: "", age: undefined },
});

// In a server action / load:
const state = await Effect.runPromise(signupForm.validate(request));
// state: { id, valid, posted, data, errors, constraints, message? }

Building a form incrementally

Form.make binds a hand-written Schema.Struct. When you'd rather compose a form field by field — reusing field groups, merging them, and attaching whole-form (cross-field) rules — reach for FormBuilder:

import * as Schema from "effect/Schema";
import { Field, Form, FormBuilder } from "@thomasfosterau/effect-forms";

const signup = FormBuilder.empty
  .addField("email", Schema.String.check(Schema.isMinLength(3)))
  .addField("password", Schema.String.check(Schema.isMinLength(8)))
  .addField("confirm", Schema.String)
  .addField(Field.makeArrayField("tags", Schema.String)) // an array field
  .refine((v) =>
    v.password === v.confirm ? undefined : { path: ["confirm"], issue: "Passwords must match" },
  );

// Bind it exactly like a schema (defaults are derived when omitted):
const form = Form.fromBuilder(signup, { id: "signup" });
const state = await Effect.runPromise(form.validate(request));

FormBuilder.buildSchema collapses the builder into an ordinary Schema.Struct (array fields become Schema.Array(item)) with each synchronous refine applied as a struct-level check — so it decodes, derives constraints, and validates like any schema. refineEffect attaches an asynchronous whole-form rule (a uniqueness check against a service, say); Effect Schema has no effectful filter, so Form.fromBuilder runs those on the Effect validate path and folds their failures into the same error tree.

Modules are exposed as Effect-style namespaces (Form, State, Result, Errors, …). Form.make(schema, { defaults }) is the server-side validate-and-seed entry point:

  • validate(input) accepts a FormData, URLSearchParams, Request, or a plain object. It parses the body (nested author.name / tags[0] / tags[] notation), decodes it through the schema collecting every field error (errors: "all"), and returns a serialisable FormState — valid with the decoded data, or invalid with the nested FormErrors tree and the posted values preserved so the form re-renders what the user typed. It is an Effect, so a schema that requires decoding services composes naturally; it never fails on the error channel (validation failures live in the returned state).
  • empty() seeds a blank, unposted form from the defaults — what a load returns to render a create form.

What's in the box

| Module | Purpose | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Form | make — bind a schema to a form (validate + empty); fromBuilder — bind a FormBuilder. | | FormBuilder | Compose a form field by field: addField / merge / refine / refineEffect / buildSchema. | | Field | makeField / makeArrayField field defs + guards, and blank-default derivation from a schema — defaultValue / defaultEncodedValues for a builder field, schemaAt / defaultItemAt for any nested path. | | State | FormState, makeFormState, setMessage, withError, FormStateSchema (transport). | | Errors | The nested FormErrors tree + issuesToErrors / flattenErrors / getError / setError / mergeErrors / remapIndices; FieldValidators per-source error tracking via replaceAt / setSourceAt / clearSourceAt / mergeSourced. | | Issues | FormIssue + the Effect Schema issue-tree walker. | | FormData | formDataToObject / parseFieldPath — structured parsing of an HTML body. | | Path | getAt / setAt (immutable) along a FormPath, toString / fromString notation conversion, the type-level FieldPath<T> / PathValue<T, P> deep-key projections a typed field handle is built on, and the IndexMap constructors (removeAt / insertAt / swapAt / moveAt / replaceAt / clearAll) + remapIndex / remapKeyedRecord for keeping per-index state in sync with an array mutation. | | Coerce | Tri-state (value / null / undefined) coercers for the form boundary. | | Constraints | constraintsFromSchema — native input attributes (required, minlength/maxlength, min/max, pattern, step) derived from the schema AST, including into array items; constraintAt reads one field's leaf out of the derived tree. | | Mode | FormMode + parse — when a form validates (onSubmit / onBlur / onChange), whether it auto-submits, and when a posted form revalidates (revalidate). | | Result | FormActionResult — the framework-neutral submission outcome (success / failure / redirect / error). | | Validate | validateValue / validateValueSync — ad-hoc value/field validation; FieldValidators + runSyncFieldValidator / runAsyncFieldValidator — per-field sync/async validators (debounced, fiber-cancellable). |

Notes

  • Coercion is the schema's job. An HTML body is all strings; build the schema from coercing leaves (Schema.FiniteFromString, Schema.BooleanFromString, the Coerce helpers) so the parsed object decodes cleanly. The parser never guesses types.
  • Errors nest by field. Leaf fields are string[]; nested objects / array indices nest; a branch's own (cross-field) message lives under _errors. The tree is plain JSON — it crosses the SSR boundary unchanged.
  • Update semantics. The Coerce helpers honour the value / null / undefined contract (set / clear / carry-forward) the app uses for partial updates.
  • Constraint derivation reads schema checks, not just optionality. constraintsFromSchema walks each property's .check()s for the meta annotation Effect Schema's built-in checks carry (isMinLength, isLengthBetween, isPattern, isGreaterThanOrEqualTo, isBetween, isMultipleOf, isInt, …), unwraps optional / NullOr unions, and descends into struct and array-item shapes. A check with no recognised meta tag (a bare Schema.filter predicate) derives nothing — there's no attribute to guess at. Anything the walk can't (or shouldn't) infer goes through the overrides argument, which always wins on conflict.
  • Errors have a source. Errors.SourcedErrors keeps a client tree (browser-side validation) parallel to a server tree (the last submission), merged for display by mergeSourced. Errors.replaceAt / setSourceAt / clearSourceAt update one path in one source without disturbing the rest — what a reactive store needs so a server-reported failure survives a client revalidation of some other field, but clears once its own field changes.

File naming

Source modules use PascalCase file names, matching Effect's own convention (effect/Schema, effect/SchemaParser). The package has a single entry point: every module is re-exported from the . barrel, so there are no subpaths to import from.

Development

From the repository root:

vp run -r build   # build every package with tsdown
vp check          # format (Oxfmt), lint (Oxlint), type-check (tsgolint)
vp run -r test    # unit tests

Or from this directory: vp test run / vp test run --coverage.