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, '')
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)
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
}
}
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
}
}
}
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
Try it
npm install form-sanitize
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)