A Laravel-Inspired Approach — The Missing Piece in Your NestJS Security Arsenal

Coming from Laravel, one thing you’ll miss in NestJS is the built-in password breach validation. When I switched frameworks, I spent hours figuring out how to replicate this feature. Now, I’ll save you that time by showing you exactly how to build it.
In this guide, I’ll show you how I built a secure password validation system in NestJS that checks passwords against real data breaches. By the end, you’ll have a simple decorator that makes your app significantly more secure.
Why Your App Needs This
Every day, thousands of passwords get leaked in data breaches. Your users might be reusing passwords from compromised accounts without even knowing it. A good password validator stops this before it becomes a problem.
Laravel Built-In Under the hood
When you’re working with Laravel, password breach validation is beautifully simple. You just write:
use Illuminate\Validation\Rules\Password;
Password::defaults(function () {
return Password::min(8)
->uncompromised();
});
// Usage
'password' => [Password::default()]In deep, the uncompromised password validation lives in \Illuminate\Validation\NotPwnedVerifier. Here's what the core verification code looks like:
namespace Illuminate\Validation;
class NotPwnedVerifier
{
public function verify($value, $threshold = 0)
{
$hash = strtoupper(sha1($value));
$prefix = substr($hash, 0, 5);
$response = (new \GuzzleHttp\Client)->get(
sprintf('https://api.pwnedpasswords.com/range/%s', $prefix)
)->getBody()->getContents();
foreach (explode("\n", $response) as $line) {
[$hashSuffix, $count] = explode(":", $line);
if (substr($hash, 5) === $hashSuffix && $count > $threshold) {
return false;
}
}
return true;
}
}P/s: Laravel has alot of out-of-the-box features compared to other framework 🫡
Building the NestJS Version
Now that we understand Laravel’s approach, let’s build our version in NestJS. We’ll create something that feels natural in the NestJS ecosystem while maintaining Laravel’s elegance.
We’re creating a custom decorator that checks if a password has appeared in known data breaches. When you’re done, you’ll be able to validate passwords with a simple @UncompromisedPassword() decorator - just like in Laravel.
Here is the sample what will looks like in the end :-
export class CreateUserDto {
@UncompromisedPassword()
password: string;
}Step 1: Set Up the Project Structure
First, create a new folder structure in your NestJS project:
src/
validators/
utils/
ihavebeenpwned.utils.ts
contracts/
uncompromised-password.contract.ts
uncompromised-password.service.ts
uncompromised-password.decorator.tsStep 2: Create the Password Check Function
In ihavebeenpwned.utils.ts, add the core function that talks to the HaveIBeenPwned API:
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
export const searchPwnedPasswords = async (
password: string,
httpService: HttpService,
timeout = 30000,
): Promise<boolean> => {
try {
const [hash, hashPrefix] = await getHash(password);
const threshold = 0;
const url = `https://api.pwnedpasswords.com/range/${hashPrefix}`;
const response = await firstValueFrom(
httpService.get(url, {
headers: {
'Add-Padding': 'true',
},
timeout,
}),
);
if (response.status !== 200) {
throw new Error('Failed to check password against HaveIBeenPwned API');
}
const responseData = response.data;
if (typeof responseData !== 'string') {
return true;
}
const results = responseData
.trim()
.split('\n')
.filter((line) => line.includes(':'));
return !results.some((line) => {
const [hashSuffix, count] = line.split(':');
return hashPrefix + hashSuffix === hash && parseInt(count) > threshold;
});
} catch (error) {
console.error('API request failed:', error);
throw error;
}
};
// Private helper function to generate hash
const getHash = async (value: string): Promise<[string, string]> => {
try {
const encoder = new TextEncoder();
const data = encoder.encode(value);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hash = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
const hashPrefix = hash.slice(0, 5);
return [hash, hashPrefix];
} catch (error) {
console.error('Hash generation failed:', error);
throw error;
}
};This function does the heavy lifting of checking passwords against the breach database. Mimic every step from NotPwnedVerifier into typescript syntax.
Step 3: Define the Contract
Create uncompromised-password.contract.ts to define the validator's options:
export type UncompromisedPasswordContract = {
options: [];
};This might look simple now but it gives us room to add more options later.
Step 4: Create the Validator Service
In uncompromised-password.service.ts, create the validator class:
import { Injectable } from '@nestjs/common';
import {
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { PrismaService } from '../../database/prisma.service';
import { HttpService } from '@nestjs/axios';
import { searchPwnedPasswords } from '../utils/ihavebeenpwned.utils';
@ValidatorConstraint({ name: 'UncompromisedPasswordValidator', async: true })
@Injectable()
export class UncompromisedPasswordValidator
implements ValidatorConstraintInterface
{
constructor(
private readonly prisma: PrismaService,
private readonly httpService: HttpService,
) {}
async validate(
password: string,
args: ValidationArguments,
): Promise<boolean> {
if (!password || typeof password !== 'string') {
return false;
}
try {
return await searchPwnedPasswords(password, this.httpService);
} catch (error) {
console.error('Password verification failed:', error);
return true;
}
}
defaultMessage(args: ValidationArguments): string {
return 'The password has been exposed in data breaches. Please choose a different password.';
}
}This service connects our password checker to NestJS’s validation system.
Note that, im using Prisma. You may adjust the PrismaService change to Drizzle or TypeORM based on your needs.
Step 5: Create the Decorator
Finally, in uncompromised-password.decorator.ts, create the decorator that brings it all together:
import { registerDecorator, ValidationOptions } from 'class-validator';
import { UncompromisedPasswordContract } from '../contracts/uncompromised-password.contract';
import { UncompromisedPasswordValidator } from '../uncompromised-password.service';
export function UncompromisedPassword(
options?: UncompromisedPasswordContract,
validationOptions?: ValidationOptions,
) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'UncompromisedPassword',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [options],
validator: UncompromisedPasswordValidator,
});
};
}Step 6: Set Up in DTO Model
Now comes the fun part — using your new validator. In Laravel, looks like this:-
// Laravel
public function rules()
{
return [
'password' => ['required', Password::min(8)->uncompromised()]
];
}In your NestJS DTO model will look similar like this:
import { UncompromisedPassword } from './validators/uncompromised-password.decorator';
export class CreateUserDto {
@UncompromisedPassword()
password: string;
}Make sure to register the validator and HttpService in your module:
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { UncompromisedPasswordValidator } from './validators/uncompromised-password.service';
@Module({
imports: [HttpModule],
providers: [UncompromisedPasswordValidator],
})
export class AppModule {}Key Differences Between Laravel and NestJS Implementations
- Dependency Injection: Laravel uses static methods, while NestJS uses dependency injection
- Error Handling: NestJS implementation includes more robust error handling
- Type Safety: Our NestJS version adds TypeScript’s type checking
- Customization: Both allow for customization but through different patterns
- Integration: Laravel integrates with the validation system directly, while NestJS uses class-validator decorators
Security and Performance Tips
After using this in production, here’s what I learned:
- Set up proper error handling — the API might be down sometimes
- Monitor your API calls — HaveIBeenPwned has rate limits
- Consider caching responses — but be careful with hash storage
- Keep an eye on timeout values — some connections might be slow
Looking Forward
You’ve now replicated one of Laravel’s password security features in NestJS. The implementation might be different but the end result is the same: better security for your users.
Coming from Laravel? Share your experience adapting other Laravel features to NestJS in the comments below.
If you found this article insightful and want to stay updated with more content on system design and technology trends, be sure to follow me on :-
Twitter: https://twitter.com/hafiqdotcom
LinkedIn: https://www.linkedin.com/in/hafiq93
Buy Me Coffee: https://paypal.me/mhi9388 /
https://buymeacoffee.com/mhitech
Medium: https://medium.com/@hafiqiqmal93
Thank you for being a part of the community
Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast
- Check out CoFeed, the smart way to stay up-to-date with the latest in tech 🧪
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com