ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส
เขียนกฎ @ValidatorConstraint ที่นำกลับมาใช้ใหม่ได้ รวมถึงการตรวจสอบแบบ async กับฐานข้อมูล
ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส เป็นบทเรียน NestJS Enterprise Backend APIs ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน NestJS Enterprise Backend APIs และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
Why Custom Validators?
The class-validator decorators that NestJS ships with (@IsEmail, @Min, @Length) cover generic shapes, but enterprise rules are domain-specific: "this coupon code must exist and not be expired" or "this email must be unique in the users table".
- Custom validators let you encapsulate such rules behind a single reusable decorator.
- They keep DTOs declarative and your business logic out of controllers.
- Two flavors exist: inline (
@Validate/registerDecorator) and constraint classes (@ValidatorConstraint).
In this lesson we focus on the @ValidatorConstraint class approach, including async checks that hit a database.
Anatomy of a ValidatorConstraint
A constraint class implements ValidatorConstraintInterface and is decorated with @ValidatorConstraint. It exposes two methods:
validate(value, args)returnsboolean(orPromise<boolean>for async).defaultMessage(args)returns the error string when validation fails.
The name option is the rule identifier, and async: true tells class-validator to await the result.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint
implements ValidatorConstraintInterface
{
validate(value: string, _args: ValidationArguments): boolean {
if (typeof value !== 'string') return false;
const hasUpper = /[A-Z]/.test(value);
const hasDigit = /[0-9]/.test(value);
return value.length >= 8 && hasUpper && hasDigit;
}
defaultMessage(args: ValidationArguments): string {
return `${args.property} must be 8+ chars with an uppercase letter and a digit`;
}
}Wrapping It in a Decorator
Implementing the constraint is only half the story. To get a clean @IsStrongPassword() decorator you wrap registerDecorator in a factory function.
registerDecoratorbinds your constraint class to a target property.validationOptionslets callers override the message per-field.- The factory returns a
PropertyDecorator, so it reads like a built-in decorator on the DTO.
import { registerDecorator, ValidationOptions } from 'class-validator';
import { IsStrongPasswordConstraint } from './is-strong-password.constraint';
export function IsStrongPassword(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [],
validator: IsStrongPasswordConstraint,
});
};
}
// Usage in a DTO:
// class CreateUserDto {
// @IsStrongPassword()
// password: string;
// }The Pure Logic Is Testable
The core of a validator is plain TypeScript with no framework coupling. You can extract and unit-test the predicate in isolation. Below is a standalone program demonstrating the strong-password rule.
function isStrongPassword(value: string): boolean {
if (typeof value !== 'string') return false;
const hasUpper = /[A-Z]/.test(value);
const hasDigit = /[0-9]/.test(value);
return value.length >= 8 && hasUpper && hasDigit;
}
const cases = ['abc', 'alllowercase1', 'Short1', 'GoodPass99'];
for (const c of cases) {
console.log(`${c.padEnd(15)} -> ${isStrongPassword(c)}`);
}Passing Arguments to a Constraint
Validators often need parameters: a minimum age, an allowed currency list, or a sibling field to compare against. You pass them via the constraints array, then read them inside validate through args.constraints.
A classic example is @Match, which checks one property equals another (e.g. passwordConfirm equals password).
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'match', async: false })
export class MatchConstraint implements ValidatorConstraintInterface {
validate(value: unknown, args: ValidationArguments): boolean {
const [relatedProperty] = args.constraints as [string];
const related = (args.object as Record<string, unknown>)[relatedProperty];
return value === related;
}
defaultMessage(args: ValidationArguments): string {
const [relatedProperty] = args.constraints as [string];
return `${args.property} must match ${relatedProperty}`;
}
}
export function Match(property: string, options?: ValidationOptions) {
return (object: object, propertyName: string) =>
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [property],
validator: MatchConstraint,
});
}Going Async: Database Constraints
The real enterprise power is async validation: checking a value against the database during request validation. The most common case is uniqueness.
- Set
async: truein@ValidatorConstraint. - Return a
Promise<boolean>fromvalidate. - Inject a repository/service into the constraint class.
For dependency injection to work, the constraint must be injectable and class-validator must use Nest's container (covered in the next scene).
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
import { User } from './user.entity';
@ValidatorConstraint({ name: 'isEmailUnique', async: true })
@Injectable()
export class IsEmailUniqueConstraint
implements ValidatorConstraintInterface
{
constructor(
@InjectRepository(User) private readonly users: Repository<User>,
) {}
async validate(email: string): Promise<boolean> {
const existing = await this.users.findOne({ where: { email } });
return existing === null;
}
defaultMessage(args: ValidationArguments): string {
return `email '${args.value}' is already registered`;
}
}Wiring DI With useContainer
By default class-validator instantiates constraint classes itself, so @Injectable() dependencies are undefined. You must tell class-validator to resolve constraints through Nest's DI container.
- Call
useContainer(app.select(AppModule), { fallbackOnErrors: true })inmain.ts. - Register the constraint as a provider in the module that owns the repository.
fallbackOnErrors: truelets non-injectable built-in validators still work.
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Resolve custom validators via Nest's DI container
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.listen(3000);
}
bootstrap();Registering the Constraint as a Provider
The injectable constraint only gets its dependencies if Nest knows about it. Add it to the module providers array alongside the feature it validates.
- Import
TypeOrmModule.forFeature([User])so the repository token is available. - List
IsEmailUniqueConstraintinproviders. - If other modules build DTOs using this rule,
exportit.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { IsEmailUniqueConstraint } from './is-email-unique.constraint';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, IsEmailUniqueConstraint],
exports: [IsEmailUniqueConstraint],
})
export class UsersModule {}Using the Async Decorator on a DTO
Once wired, the async rule reads exactly like a sync one. The global ValidationPipe awaits it automatically, so a duplicate email yields a 422/400 response before your controller ever runs.
Compose it with standard decorators — order does not matter, all run and collected errors are merged.
import { IsEmail, IsNotEmpty } from 'class-validator';
import { IsEmailUnique } from './is-email-unique.decorator';
import { IsStrongPassword } from './is-strong-password.decorator';
export class RegisterUserDto {
@IsEmail()
@IsEmailUnique({ message: 'This email is taken' })
email: string;
@IsNotEmpty()
@IsStrongPassword()
password: string;
}Performance and TOCTOU Caveats
Async DB validators are convenient but carry trade-offs you must design around in production:
- Extra query per request — each async rule is a round trip; avoid stacking many on hot endpoints.
- Race condition (TOCTOU) — between the validation read and the actual
INSERT, another request can claim the value. The check is not a substitute for a realUNIQUEconstraint. - Always keep a database-level unique index and catch the duplicate error as the final guard.
Treat async validators as a UX nicety that returns friendly field errors, not as the source of truth for integrity.
Returning Friendly, Targeted Errors
A well-built constraint produces messages tied to the failing field, which the ValidationPipe aggregates into a structured response. You can interpolate the value and property via ValidationArguments.
This standalone snippet simulates how a constraint's validate/defaultMessage pair would behave against a fake user store.
const existingEmails = new Set(['ada@corp.io', 'grace@corp.io']);
function validateUnique(email: string): { ok: boolean; message?: string } {
const ok = !existingEmails.has(email);
return ok ? { ok } : { ok, message: `email '${email}' is already registered` };
}
for (const email of ['ada@corp.io', 'linus@corp.io']) {
const result = validateUnique(email);
console.log(`${email.padEnd(16)} -> ${JSON.stringify(result)}`);
}Quick Check
You added an injectable @ValidatorConstraint({ async: true }) class that injects a TypeORM repository, but at runtime the repository is undefined and the app throws. What is the missing step?
Recap
You can now build reusable, framework-grade validators in NestJS:
- Implement
ValidatorConstraintInterfacewithvalidateanddefaultMessage, decorated by@ValidatorConstraint({ name, async }). - Wrap it in a factory using
registerDecoratorfor a clean@MyRule()decorator, passing parameters via theconstraintsarray. - For async DB checks, make the constraint
@Injectable(), register it as a provider, and calluseContainer(...)inmain.tsso DI works. - Remember the TOCTOU caveat: async uniqueness validators improve UX but must be backed by a database
UNIQUEconstraint for true integrity.
เรียนรู้ TypeScript ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 20
- บทเรียน
- 76
คำถามที่พบบ่อย
บทเรียน “ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส NestJS Enterprise Backend APIs ให้อัปเกรดเป็น CoddyKit PRO คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส”
เขียนกฎ @ValidatorConstraint ที่นำกลับมาใช้ใหม่ได้ รวมถึงการตรวจสอบแบบ async กับฐานข้อมูล คุณปฏิบัติ NestJS Enterprise Backend APIs ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน NestJS Enterprise Backend APIs หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน NestJS Enterprise Backend APIs บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน NestJS Enterprise Backend APIs นี้ได้ไหม
ได้ บทเรียน NestJS Enterprise Backend APIs ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การตรวจสอบ DTO ซ้อนกันและอาร์เรย์
- ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส
- การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor
- การตรวจสอบแบบมีเงื่อนไขและกลุ่มแบบไดนามิก