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

@cassets/form-pipeline

v0.1.1

Published

DOM-first form-to-payload pipeline for React and browser apps with native validation, coercion, nested payloads, files, and optional Zod validation.

Readme

@cassets/form-pipeline

DOM-first form-to-payload processing for React and browser applications.

@cassets/form-pipeline reads the form you already rendered, applies native HTML validation, normalizes and coerces values, builds nested payloads, handles files, and optionally validates with Zod. It is designed for uncontrolled or lightly controlled forms where the DOM is the source of truth.

npm license

Why this package?

Traditional React forms often duplicate browser state in useState, repeat validation rules in multiple layers, and hand-build API payloads. This package uses the semantics already present in HTMLFormElement controls.

HTMLFormElement
  -> discover controls
  -> read raw values
  -> normalize
  -> coerce from HTML semantics
  -> native validation
  -> transforms
  -> nested builder
  -> optional Zod validation
  -> JSON object or FormData

The package can build a payload only, or the React hook can optionally call your submit function / URL. For larger applications, keeping network submission in your API layer is usually the cleanest boundary.

Installation

npm install @cassets/form-pipeline react

Optional Zod validation (any compatible safeParse schema can be used):

npm install zod

React >=17 is a peer dependency. Zod >=3 is optional.

Quick start: payload only

import { useRef } from 'react';
import { formPipe } from '@cassets/form-pipeline';

export function ProfileForm() {
  const ref = useRef<HTMLFormElement>(null);

  return (
    <form
      ref={ref}
      onSubmit={(event) => {
        event.preventDefault();
        if (!ref.current) return;

        const result = formPipe(ref.current);
        if (!result.success) {
          console.error(result.errors);
          return;
        }

        console.log(result.payload);
      }}
    >
      <input name="profile.name" required />
      <input name="profile.age" type="number" min={1} />
      <button>Save</button>
    </form>
  );
}

For input values profile.name="Vivek" and profile.age="37", the payload is:

{
  "profile": {
    "name": "Vivek",
    "age": 37
  }
}

React hook

import { useRef } from 'react';
import { useForm } from '@cassets/form-pipeline';

export function LoginForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const { handleSubmit, errors, isSubmitting, isSuccess } = useForm({
    formRef,
    submit: async (payload) => {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    },
  });

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      {errors?.email && <p>{errors.email}</p>}

      <input name="password" type="password" required minLength={8} />
      {errors?.password && <p>{errors.password}</p>}

      <button disabled={isSubmitting}>
        {isSubmitting ? 'Signing in...' : 'Sign in'}
      </button>
      {isSuccess && <p>Signed in</p>}
    </form>
  );
}

You can use either a React ref or a selector:

useForm({ formRef: 'form#account' });

Core API

formPipe(form, options?, schema?)

Runs the full pipeline synchronously.

const result = formPipe<MyPayload>(formElement, options, schema);

Result:

interface PipelineResult<T> {
  success: boolean;
  payload: T;
  errors: Record<string, string> | null;
  rawValues: Record<string, unknown>;
}

rawValues contains values as read from controls before normalization/coercion/transforms. payload is the final value.

toFormData(payload)

Converts a nested payload into FormData using dotted keys.

import { toFormData } from '@cassets/form-pipeline';

const formData = toFormData({
  profile: { name: 'Vivek' },
  avatar: file,
});

// profile.name -> Vivek
// avatar       -> File

useForm(config)

const {
  handleSubmit,
  getPayload,
  reset,
  setFieldError,
  clearErrors,
  payload,
  errors,
  isSubmitting,
  isSuccess,
  isError,
} = useForm({
  formRef,
  schema,
  submit,
  url,
  method,
  submitAs,
  options,
  onSuccess,
  onError,
  resetOnSuccess,
});

getPayload() validates the current form and returns either the payload, FormData, or null when validation fails.

HTML control behavior

Text, email, password, textarea

<input name="name" required />
<input name="email" type="email" required />
<textarea name="bio" maxlength="500"></textarea>

Strings are trimmed by default. Empty strings become undefined by default.

Number and range

<input name="quantity" type="number" min="1" max="99" />
<input name="score" type="range" min="0" max="10" />

number and range values are converted to JavaScript numbers by default. Numeric-looking text fields remain strings.

Single checkbox

<input name="termsAccepted" type="checkbox" required />

Produces a boolean:

{ "termsAccepted": true }

Checkbox group

<label><input type="checkbox" name="roles" value="admin" /> Admin</label>
<label><input type="checkbox" name="roles" value="editor" /> Editor</label>
<label><input type="checkbox" name="roles" value="viewer" /> Viewer</label>

Checked values become an array:

{ "roles": ["admin", "viewer"] }

Radio group

<label><input type="radio" name="plan" value="free" /> Free</label>
<label><input type="radio" name="plan" value="pro" /> Pro</label>

Only the checked radio contributes a value.

Select and multi-select

<select name="country" required>
  <option value="IN">India</option>
  <option value="GB">United Kingdom</option>
</select>

<select name="skills" multiple>
  <option value="typescript">TypeScript</option>
  <option value="react">React</option>
  <option value="node">Node.js</option>
</select>

A multi-select produces a string array.

Date controls

Dates remain strings by default, which is usually safest for APIs.

<input name="startDate" type="date" />

To produce Date objects:

formPipe(form, {
  coerce: { dates: 'Date' },
});

File and multiple files

<input name="avatar" type="file" accept="image/*" />
<input name="documents" type="file" multiple />

Single-file controls produce File; multiple-file controls produce File[].

For transport use FormData:

const { handleSubmit } = useForm({
  formRef,
  submitAs: 'formdata',
  submit: async (body) => fetch('/api/profile', { method: 'POST', body }),
});

Do not manually set the multipart Content-Type header; the browser adds its boundary.

Nested objects and arrays

Dot and bracket notation are supported:

<input name="user.name" value="Vivek" />
<input name="user.address.city" value="Chennai" />
<input name="items[0].sku" value="A-100" />
<input name="items[0].quantity" type="number" value="2" />
<input name="items[1].sku" value="B-200" />

Produces:

{
  "user": {
    "name": "Vivek",
    "address": { "city": "Chennai" }
  },
  "items": [
    { "sku": "A-100", "quantity": 2 },
    { "sku": "B-200" }
  ]
}

Dangerous object path segments such as __proto__, constructor, and prototype are rejected by the nested builder.

Native HTML validation

The browser remains the first validation layer:

<input name="username" required minlength="3" maxlength="30" />
<input name="email" type="email" required />
<input name="age" type="number" min="18" max="120" />
<input name="code" pattern="[A-Z]{3}-[0-9]{4}" />

Invalid controls are returned in errors using the browser's validationMessage.

Zod validation

import { z } from 'zod';
import { useForm } from '@cassets/form-pipeline';

const schema = z.object({
  email: z.string().email(),
  age: z.number().int().min(18),
});

const form = useForm<z.infer<typeof schema>>({
  formRef,
  schema,
});

Native validation runs first at control level; Zod validates the assembled payload.

Normalization and coercion

Defaults:

{
  normalize: {
    trim: true,
    emptyToUndefined: true,
    emptyToNull: false,
  },
  coerce: {
    numbers: true,
    booleans: true,
    dates: 'string',
  },
  nested: true,
  skipUnderscore: true,
  skipAttributes: ['data-skip'],
  skipDisabled: true,
  stripEmpty: true,
}

Disable behavior explicitly:

const result = formPipe(form, {
  normalize: { trim: false, emptyToUndefined: false },
  coerce: { numbers: false },
  nested: false,
  stripEmpty: false,
});

Transform fields

const result = formPipe(form, {
  transform: {
    email: (value) => String(value).toLowerCase(),
  },
  fields: {
    amount: {
      transform: (value) => Math.round(Number(value) * 100),
    },
  },
});

Field-specific transforms run before the global transform map for the same field.

Excluding controls

data-skip

<input name="uiSearch" data-skip />

Leading underscore

<input name="_csrf_display_only" />

Names beginning with _ are skipped by default.

Ignore exact names or wildcard patterns

formPipe(form, {
  ignoreFields: ['debug', 'internal.*', 'items.*.temporary'],
});

Per-field skip

formPipe(form, {
  fields: {
    internalToken: { skip: true },
  },
});

Custom inclusion logic

formPipe(form, {
  shouldInclude: ({ fieldName, element, rawValue }) => {
    return !element.closest('[data-disabled-section]');
  },
});

Direct URL submission

For small applications, useForm can own the final fetch call:

const form = useForm({
  formRef,
  url: '/api/users',
  method: 'POST',
  onSuccess: (response) => console.log(response),
  onError: (error) => console.error(error),
  resetOnSuccess: true,
});

For authentication, retries, caching, request cancellation, or application-wide error handling, prefer the submit callback and your own HTTP layer.

Application-defined server errors

const { setFieldError, clearErrors } = useForm({ formRef });

setFieldError('email', 'This email is already registered');
clearErrors();

Reset

reset() calls the native form reset() and clears hook state. resetOnSuccess: true does the same after a successful hook-owned submission.

Browser and SSR notes

The processing APIs depend on browser DOM types such as HTMLFormElement, File, and FormData. Run them client-side. Importing types is safe, but do not execute the form pipeline during server rendering.

Security model

  • The package does not send credentials unless you configure useForm submission.
  • Native browser validation is a UX/client-side guard, not a server security boundary.
  • Always validate and authorize again on the server.
  • Nested path construction blocks prototype-pollution path segments.
  • File accept is advisory; validate file type, size, and content on the server.

Package family

Project links

  • ClusterAssets GitHub: https://github.com/clusterassets
  • ClusterAssets LinkedIn: https://www.linkedin.com/company/clusterassets
  • Creator GitHub: https://github.com/diskhacker
  • Creator LinkedIn: https://www.linkedin.com/in/kp-vivek-rao-bhosale/

Contributing and security

See CONTRIBUTING.md and SECURITY.md.

License

MIT © Vivek Rao Bhosale / ClusterAssets.