DEV Community

Cover image for I just published my first npm package — and it actually solves a real problem
tnikhil-24
tnikhil-24

Posted on

I just published my first npm package — and it actually solves a real problem

Tags: javascript typescript npm webdev


I'm a fresher. No years of experience. No open source track record.

But yesterday, I typed npm publish for the first time — and something I built from scratch went live for anyone in the world to install.

Here's the story of what I built, why it exists, and how it works.


The problem that started it all

Every project has a contact form. Every single one. And every single time, I'd write the same defensive code:

const name = req.body.name.trim()
const email = req.body.email.toLowerCase().trim()
const message = req.body.message.replace(/<[^>]*>/g, '')
Enter fullscreen mode Exit fullscreen mode

Copy. Paste. Tweak. Repeat. On the frontend for UX. On the backend for security. Twice, every time, for every project.

So I went looking for a package that solved this properly.

DOMPurify — great for XSS, but browser-only. No Node.js, no form field awareness.

validator.js — 70+ loose functions, no composable API, call each one manually on each field.

HTML Sanitizer API — browser-only, limited support, still no form object support.

None of them let you define a schema for your entire form and sanitize it in one shot — on both frontend and backend.

That gap is exactly what I built form-sanitize to fill.


What it looks like

import { createSchema, s } from 'form-sanitize'

const contactForm = createSchema({
  name:    s.string().trim().stripTags(),
  email:   s.string().trim().normalizeEmail(),
  age:     s.number().clamp(0, 120),
  active:  s.boolean(),
  address: s.object({
    city: s.string().trim(),
    zip:  s.string().truncate(10),
  }),
})

// Works in Express, Next.js API routes, React — anywhere
const clean = contactForm.sanitize(req.body)
Enter fullscreen mode Exit fullscreen mode

Define your rules once. Run them on the server. Reuse them on the client. Nested objects included. Zero dependencies. Full TypeScript support with type inference.


The full API

s.string()

Method What it does
.trim() Removes leading and trailing whitespace
.stripTags() Strips HTML and script tags including their content
.truncate(n) Caps the string at n characters
.normalizeEmail() Lowercases, removes Gmail dots and plus aliases
.escape() HTML-encodes special characters
.toSlug() Converts to a URL-safe slug

s.number()

Method What it does
.clamp(min, max) Keeps the value within a range
.round(decimals) Rounds to given decimal places
.abs() Makes the value positive

s.boolean()

Intelligently coerces messy input — "true", "yes", "1", 1 all become true. "false", "no", "0", 0 all become false. No more req.body.active === 'true' hacks.

s.object(definition)

Sanitizes nested objects recursively. Handles null, undefined, and completely missing fields gracefully — returns clean empty defaults instead of throwing.


How it works under the hood

The core is the builder pattern. Every method stores a rule and returns this — enabling the chain:

class StringField {
  private rules: Array<(val: string) => string> = []

  trim(): this {
    this.rules.push((val) => val.trim())
    return this // ← this one line is what enables chaining
  }

  sanitize(value: unknown): string {
    let val = String(value ?? '')
    for (const rule of this.rules) {
      val = rule(val)
    }
    return val
  }
}
Enter fullscreen mode Exit fullscreen mode

When you write s.string().trim().stripTags() you're not sanitizing anything yet — you're building a blueprint. The actual work only happens when schema.sanitize(data) is called.

The schema engine then maps field names to their blueprints and runs them:

function createSchema(definition) {
  return {
    sanitize(data) {
      const result = {}
      for (const key in definition) {
        result[key] = definition[key].sanitize(data[key])
      }
      return result
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Clean, composable, and easy to extend with new field types in the future.


Why this is different from existing tools

DOMPurify validator.js form-sanitize
Works in Node.js
Schema-based
Chainable API
Nested objects
Zero dependencies

This isn't trying to replace DOMPurify for rich text — pair them if you need deep HTML sanitization. form-sanitize is for form data — names, emails, ages, addresses, checkboxes — the stuff every app collects.


The numbers

43 tests passing
0 dependencies
2 build formats — ESM + CJS
Works in Node.js and browser
Full TypeScript type inference
Enter fullscreen mode Exit fullscreen mode

Try it

npm install form-sanitize
Enter fullscreen mode Exit fullscreen mode

GitHub: github.com/tnikhil-24/form-sanitize

npm: npmjs.com/package/form-sanitize

If you've ever copy-pasted the same .trim().toLowerCase() across your frontend and backend — this is for you.

Feedback, issues, and PRs are very welcome. I'm learning in public and this is just v1.


Built with TypeScript, tsup, and vitest. Zero dependencies, maximum usefulness.

Top comments (0)