A Quick Intro
Hey, I'm Muhammad Ali Kazmi, a Full Stack Developer from Karachi, Pakistan with 5+ years of experience building web applications. I've worked on everything from AI-powered SaaS platforms to fintech apps, mostly using React, Node.js, and TypeScript.
You can find me on GitHub, LinkedIn, or check out my work at developerkazmi.com.
Now, onto the story...
So I spent the last few weeks building an email validation library from scratch and publishing it on npm. Not because the world desperately needed another npm package, but because I wanted to deeply understand how email validation actually works. And honestly, the existing solutions frustrated me.
In this post, I'll share everything: why I built it, what I learned, the technical decisions that made it 3x faster than alternatives, and how you can use this same approach to build your own npm packages.
Let's dive in...
What I'm Going To Cover:
• Why I started this project
• The 5 layers of email validation most developers don't know about
• How I made it 3x faster than the competition
• The tech stack decisions (and why)
• How to publish your first npm package
• What I'd do differently next time
Why Build Another Email Validator?
Here's the thing. I was using deep-email-validator in a project. It worked fine, but every time I dug into the code, I found things that bothered me:
• The TypeScript types were outdated (still using 3.8)
• No bulk validation support
• No caching (validating the same email twice meant doing all the work again)
• Error messages were generic strings like "Invalid email"
• The regex validation wasn't even RFC 5322 compliant
I thought to myself: "How hard can it be to build something better?"
Famous last words.
Turns out, email validation is way more complex than most people think. But that complexity is exactly what made this project worth building.
The 5 Layers of Email Validation
Most developers think email validation is just a regex check. Run a pattern, get a boolean, done.
That's maybe 20% of proper email validation.
Here's what a production-ready email validator actually needs to check:
Layer 1: Regex (Format Validation)
Does the email look like an email? This needs to follow RFC 5322, the actual standard for email formats.
Fun fact: these are all technically valid emails:
• "john doe"@example.com (quoted strings)
• user+tag@example.com (plus addressing)
• user@[192.168.1.1] (IP address domains)
Most regex patterns people use from Stack Overflow would reject half of these.
Layer 2: Typo Detection
Is user@gmial.com a valid email format? Yes.
Does Gmail actually exist at gmial.com? No.
This layer catches common typos and suggests corrections. "Did you mean gmail.com?"
Layer 3: Disposable Email Detection
Mailinator. Guerrillamail. 10minutemail.
There are over 40,000 disposable email services out there. If you're not blocking these, enjoy your database full of fake users.
Layer 4: MX Record Validation
Even if the domain looks legit, does it actually have mail servers configured? This requires DNS lookups to check for MX records.
No MX records = domain can't receive emails = invalid.
Layer 5: SMTP Verification
The final boss. Actually connect to the mail server and ask: "Hey, does this mailbox exist?"
This is slow (network calls), unreliable (some servers block verification), and optional. But when you need to know for sure, this is how you do it.
Making It 3x Faster
Here's where it got interesting.
The original deep-email-validator validates emails sequentially. One at a time. No caching. Every validation hits the network for DNS and SMTP.
I wanted something that could process 100 emails in under 5 seconds.
Solution 1: Concurrent Processing
Instead of validating emails one by one, I built a bulk processor that runs validations in parallel with configurable concurrency.
const result = await validateBulk(emails, {
concurrency: 10,
onProgress: (completed, total) => {
console.log(`Progress: ${completed}/${total}`);
}
});
10 concurrent validations means roughly 10x faster bulk processing. Simple math, big impact.
Solution 2: Rate Limiting Built-In
But wait. If you're hammering mail servers with verification requests, you'll get blacklisted. Fast.
I implemented a token bucket rate limiter that controls requests per domain and globally:
await validateBulk(emails, {
rateLimit: {
perDomain: { requests: 10, window: 60 },
global: { requests: 100, window: 60 }
}
});
This prevents abuse while still keeping things fast.
Solution 3: Early Exit
If the regex check fails, why bother checking MX records? I added an early exit option that stops validation on first failure.
With early exit enabled, invalid emails get rejected in < 1ms instead of waiting for network calls.
Solution 4: Lazy Loading
The disposable email list has 40,000+ domains. Loading that into memory on every import is wasteful.
I implemented lazy loading. The dataset only loads when you actually try to validate against it. Faster startup, lower memory footprint.
The Tech Stack
Here's what I used and why:
TypeScript 5.3+ (strict mode)
• Modern type system with template literals
• Better inference than older versions
• Strict mode catches bugs at compile time
Node.js 20+
• Latest LTS with native ES modules
• Better performance
• Built-in fetch (finally)
Vitest (not Jest)
• 10x faster test runs
• Native ESM support
• Same API as Jest, so easy migration
tsup (not tsc directly)
• Zero config bundling
• Dual ESM + CommonJS output
• Proper tree-shaking
Custom Validation Utils (not Zod)
• Zero dependencies for runtime validation
• Smaller bundle size
• Zod-compatible API for easy migration later
The Final Results
After weeks of work, here's what I shipped:
644 tests passing with 90%+ coverage
5 validators:
• Regex (RFC 5322 compliant)
• Typo detection (suggests corrections)
• Disposable email blocking (40,000+ domains)
• MX record validation (with retry logic)
• SMTP verification (optional, for when you really need it)
Performance:
• Single validation: < 150ms (without SMTP)
• Bulk 100 emails: < 5 seconds
• Package size: ~31KB gzipped
Developer Experience:
• Full TypeScript support
• Three presets (strict, balanced, permissive)
• Detailed error messages with suggestions
• Reputation scoring (0-100)
Publishing to npm
This was my first npm package, so here's what I learned:
Step 1: Get Your package.json Right
{
"name": "@mailtester/core",
"version": "1.0.0",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "README.md", "LICENSE"]
}
The files field is crucial. It controls what actually gets published.
Step 2: Test With npm pack
Before publishing, run npm pack to see exactly what will be in your package. Check the size. Make sure no secrets or dev files are included.
Step 3: Publish Beta First
npm publish --tag beta --access public
Publish as beta, test in a real project, fix any issues, then publish stable.
Step 4: Create GitHub Release
Tag your release, write release notes, push to GitHub. This gives users confidence that the package is maintained.
What I'd Do Differently
Start with the API design, not the implementation.
I spent a lot of time refactoring because I didn't nail down the public API first. Next time, I'd write the README before writing any code.
Write tests for the public API first.
This forces you to think about how users will actually use your library. My best tests were the ones I wrote before implementing the feature.
Don't over-engineer v1.
I had plans for plugins, caching layers, browser builds, machine learning... I cut most of it. Ship something that works, then iterate.
Try It Yourself
The package is live on npm:
npm install @mailtester/core
Basic usage:
import { validate, validateBulk } from '@mailtester/core';
// Single email
const result = await validate('user@example.com');
console.log(result.valid); // true/false
console.log(result.score); // 0-100 reputation score
// Bulk validation
const results = await validateBulk(['email1@test.com', 'email2@test.com'], {
concurrency: 10
});
Links
📦 npm: npmjs.com/package/@mailtester/core
💻 GitHub: github.com/kazmiali/mailtester
📚 Docs: kazmiali.github.io/mailtester
What's Next?
I'm planning to add:
• In-memory LRU caching (v1.1)
• Enhanced reputation scoring with configurable weights
• CLI tool for quick validations
• Maybe a browser build
If you found this useful, star the repo on GitHub. It helps more than you'd think.
And if you build something cool with it, let me know. I'd love to see what you create.
Top comments (0)