diff --git a/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx b/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx
index 4780d11a..91ffdf52 100644
--- a/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx
+++ b/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx
@@ -7,7 +7,9 @@ import WithinRoute from "./WithinRoute.ts?raw";
## Guards and routes
-Arcjet can be integrated into NestJS in several places:
+Arcjet can be integrated into NestJS in several places using NestJS
+[guards](https://docs.nestjs.com/guards) or directly within the route
+controller:
- **Global guard:** Applies Arcjet rules on every request, but does not allow
you to configure rules per route.
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step1.mdx b/src/snippets/email-validation/quick-start/nestjs/Step1.mdx
new file mode 100644
index 00000000..d2f7bfd4
--- /dev/null
+++ b/src/snippets/email-validation/quick-start/nestjs/Step1.mdx
@@ -0,0 +1,20 @@
+import SelectableContent from "@/components/SelectableContent";
+
+{/* prettier-ignore */}
+
+
+```sh
+npm i @arcjet/nest
+```
+
+
+```sh
+pnpm add @arcjet/nest
+```
+
+
+```sh
+yarn add @arcjet/nest
+```
+
+
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step3.mdx b/src/snippets/email-validation/quick-start/nestjs/Step3.mdx
new file mode 100644
index 00000000..d1adf78d
--- /dev/null
+++ b/src/snippets/email-validation/quick-start/nestjs/Step3.mdx
@@ -0,0 +1,21 @@
+import SelectableContent from "@/components/SelectableContent";
+import { Code } from "@astrojs/starlight/components";
+import Step3AppModuleTS from "./Step3AppModule.ts?raw";
+import Step3ControllerTS from "./Step3Controller.ts?raw";
+
+Several files are combined here to demonstrate creating a form handler
+controller. In a real application you should split them as suggested in the
+comments.
+
+
+
+
+
+
+
+
+
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts b/src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts
new file mode 100644
index 00000000..78809634
--- /dev/null
+++ b/src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts
@@ -0,0 +1,34 @@
+import { ArcjetGuard, ArcjetModule } from "@arcjet/nest";
+import { Module } from "@nestjs/common";
+import { ConfigModule } from "@nestjs/config";
+import { APP_GUARD, NestFactory } from "@nestjs/core";
+
+@Module({
+ imports: [
+ ConfigModule.forRoot({
+ isGlobal: true,
+ }),
+ ArcjetModule.forRoot({
+ isGlobal: true,
+ key: process.env.ARCJET_KEY!,
+ rules: [
+ // We're not adding any rules here for this example, but if you did they
+ // would be the default rules wherever you use Arcjet.
+ ],
+ }),
+ ],
+ controllers: [],
+ providers: [
+ {
+ provide: APP_GUARD,
+ useClass: ArcjetGuard,
+ },
+ ],
+})
+class AppModule {}
+
+async function bootstrap() {
+ const app = await NestFactory.create(AppModule);
+ await app.listen(3000);
+}
+bootstrap();
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts b/src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts
new file mode 100644
index 00000000..21345d1c
--- /dev/null
+++ b/src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts
@@ -0,0 +1,101 @@
+import { ARCJET, type ArcjetNest, validateEmail } from "@arcjet/nest";
+import {
+ Body,
+ Controller,
+ HttpException,
+ HttpStatus,
+ Inject,
+ Injectable,
+ Logger,
+ Post,
+ Req,
+ UseInterceptors,
+} from "@nestjs/common";
+import { NoFilesInterceptor } from "@nestjs/platform-express";
+import { IsNotEmpty } from "class-validator";
+import type { Request } from "express";
+
+// Validation class as described at
+// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail
+// decorator here because Arcjet handles this for you.
+export class SignupDto {
+ @IsNotEmpty()
+ // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned
+ email: string;
+}
+
+// This would normally go in your service file e.g.
+// src/signup/signup.service.ts
+@Injectable()
+export class SignupService {
+ private readonly logger = new Logger(SignupService.name);
+
+ signup(email: string): { message: string } {
+ this.logger.log(`Form submission: ${email}`);
+
+ return {
+ message: "Hello world",
+ };
+ }
+}
+
+// This would normally go in your controller file e.g.
+// src/signup/signup.controller.ts
+@Controller("signup")
+export class SignupController {
+ private readonly logger = new Logger(SignupController.name);
+
+ constructor(
+ private readonly signupService: SignupService,
+ @Inject(ARCJET) private readonly arcjet: ArcjetNest,
+ ) {}
+
+ // Implement a form handler following
+ // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't
+ // compatible with the NestJS Fastify adapter.
+ @Post()
+ @UseInterceptors(NoFilesInterceptor())
+ async index(@Req() req: Request, @Body() body: SignupDto) {
+ const decision = await this.arcjet
+ .withRule(
+ validateEmail({
+ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
+ // block disposable, invalid, and email addresses with no MX records
+ block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"],
+ }),
+ )
+ .protect(req, { email: body.email });
+
+ this.logger.log(`Arcjet: id = ${decision.id}`);
+ this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
+
+ if (decision.isDenied()) {
+ if (decision.reason.isEmail()) {
+ this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`);
+
+ let message: string;
+
+ // These are specific errors to help the user, but will also reveal the
+ // validation to a spammer.
+ if (decision.reason.emailTypes.includes("INVALID")) {
+ message = "email address format is invalid. Is there a typo?";
+ } else if (decision.reason.emailTypes.includes("DISPOSABLE")) {
+ message = "we do not allow disposable email addresses.";
+ } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) {
+ message =
+ "your email domain does not have an MX record. Is there a typo?";
+ } else {
+ // This is a catch all, but the above should be exhaustive based on the
+ // configured rules.
+ message = "invalid email.";
+ }
+
+ throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST);
+ } else {
+ throw new HttpException("Forbidden", HttpStatus.FORBIDDEN);
+ }
+ }
+
+ return this.signupService.signup(body.email);
+ }
+}
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step4.mdx b/src/snippets/email-validation/quick-start/nestjs/Step4.mdx
new file mode 100644
index 00000000..7ee1b731
--- /dev/null
+++ b/src/snippets/email-validation/quick-start/nestjs/Step4.mdx
@@ -0,0 +1,30 @@
+import { Aside } from "@astrojs/starlight/components";
+import SelectableContent from "@/components/SelectableContent";
+
+### 4. Start app
+
+{/* prettier-ignore */}
+
+
+```sh
+npm run start
+```
+
+
+```sh
+pnpm run start
+```
+
+
+```sh
+yarn run start
+```
+
+
+
+Make a `curl` `POST` request from your terminal to your application with various
+emails to test the result.
+
+```shell
+curl -X POST -d 'email=test@arcjet.io' http://localhost:3000/signup/
+```
diff --git a/src/snippets/email-validation/reference/nestjs/DecisionLog.mdx b/src/snippets/email-validation/reference/nestjs/DecisionLog.mdx
new file mode 100644
index 00000000..c4979e93
--- /dev/null
+++ b/src/snippets/email-validation/reference/nestjs/DecisionLog.mdx
@@ -0,0 +1,9 @@
+import SelectableContent from "@/components/SelectableContent";
+import { Code } from "@astrojs/starlight/components";
+import DecisionLogTS from "./DecisionLog.ts?raw";
+
+
+
+
+
+
diff --git a/src/snippets/email-validation/reference/nestjs/DecisionLog.ts b/src/snippets/email-validation/reference/nestjs/DecisionLog.ts
new file mode 100644
index 00000000..4e96bcb5
--- /dev/null
+++ b/src/snippets/email-validation/reference/nestjs/DecisionLog.ts
@@ -0,0 +1,126 @@
+import {
+ ARCJET,
+ type ArcjetNest,
+ detectBot,
+ validateEmail,
+} from "@arcjet/nest";
+import {
+ Body,
+ Controller,
+ HttpException,
+ HttpStatus,
+ Inject,
+ Injectable,
+ Logger,
+ Post,
+ Req,
+ UseInterceptors,
+} from "@nestjs/common";
+import { NoFilesInterceptor } from "@nestjs/platform-express";
+import { IsNotEmpty } from "class-validator";
+import type { Request } from "express";
+
+// Validation class as described at
+// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail
+// decorator here because Arcjet handles this for you.
+export class SignupDto {
+ @IsNotEmpty()
+ // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned
+ email: string;
+}
+
+// This would normally go in your service file e.g.
+// src/signup/signup.service.ts
+@Injectable()
+export class SignupService {
+ private readonly logger = new Logger(SignupService.name);
+
+ signup(email: string): { message: string } {
+ this.logger.log(`Form submission: ${email}`);
+
+ return {
+ message: "Hello world",
+ };
+ }
+}
+
+// This would normally go in your controller file e.g.
+// src/signup/signup.controller.ts
+@Controller("signup")
+export class SignupController {
+ private readonly logger = new Logger(SignupController.name);
+
+ constructor(
+ private readonly signupService: SignupService,
+ @Inject(ARCJET) private readonly arcjet: ArcjetNest,
+ ) {}
+
+ // Implement a form handler following
+ // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't
+ // compatible with the NestJS Fastify adapter.
+ @Post()
+ @UseInterceptors(NoFilesInterceptor())
+ async index(@Req() req: Request, @Body() body: SignupDto) {
+ const decision = await this.arcjet
+ .withRule(
+ detectBot({
+ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
+ // configured with a list of bots to allow from
+ // https://arcjet.com/bot-list
+ allow: [], // blocks all automated clients
+ }),
+ )
+ .withRule(
+ validateEmail({
+ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
+ // block disposable, invalid, and email addresses with no MX records
+ block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"],
+ }),
+ )
+ .protect(req, { email: body.email });
+
+ this.logger.log(`Arcjet: id = ${decision.id}`);
+ this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
+
+ for (const result of decision.results) {
+ this.logger.log("Rule Result", result);
+
+ if (result.reason.isBot()) {
+ this.logger.log("Bot protection rule", result);
+ }
+
+ if (result.reason.isEmail()) {
+ this.logger.log("Email validation rule", result);
+ }
+ }
+
+ if (decision.isDenied()) {
+ if (decision.reason.isEmail()) {
+ this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`);
+
+ let message: string;
+
+ // These are specific errors to help the user, but will also reveal the
+ // validation to a spammer.
+ if (decision.reason.emailTypes.includes("INVALID")) {
+ message = "email address format is invalid. Is there a typo?";
+ } else if (decision.reason.emailTypes.includes("DISPOSABLE")) {
+ message = "we do not allow disposable email addresses.";
+ } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) {
+ message =
+ "your email domain does not have an MX record. Is there a typo?";
+ } else {
+ // This is a catch all, but the above should be exhaustive based on the
+ // configured rules.
+ message = "invalid email.";
+ }
+
+ throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST);
+ } else {
+ throw new HttpException("Forbidden", HttpStatus.FORBIDDEN);
+ }
+ }
+
+ return this.signupService.signup(body.email);
+ }
+}
diff --git a/src/snippets/email-validation/reference/nestjs/Errors.mdx b/src/snippets/email-validation/reference/nestjs/Errors.mdx
new file mode 100644
index 00000000..1ee27666
--- /dev/null
+++ b/src/snippets/email-validation/reference/nestjs/Errors.mdx
@@ -0,0 +1,9 @@
+import SelectableContent from "@/components/SelectableContent";
+import { Code } from "@astrojs/starlight/components";
+import ErrorsTS from "./Errors.ts?raw";
+
+
+
+
+
+
diff --git a/src/snippets/email-validation/reference/nestjs/Errors.ts b/src/snippets/email-validation/reference/nestjs/Errors.ts
new file mode 100644
index 00000000..10dde00e
--- /dev/null
+++ b/src/snippets/email-validation/reference/nestjs/Errors.ts
@@ -0,0 +1,114 @@
+import { ARCJET, type ArcjetNest, validateEmail } from "@arcjet/nest";
+import {
+ Body,
+ Controller,
+ HttpException,
+ HttpStatus,
+ Inject,
+ Injectable,
+ Logger,
+ Post,
+ Req,
+ UseInterceptors,
+} from "@nestjs/common";
+import { NoFilesInterceptor } from "@nestjs/platform-express";
+import { IsNotEmpty } from "class-validator";
+import type { Request } from "express";
+
+// Validation class as described at
+// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail
+// decorator here because Arcjet handles this for you.
+export class SignupDto {
+ @IsNotEmpty()
+ // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned
+ email: string;
+}
+
+// This would normally go in your service file e.g.
+// src/signup/signup.service.ts
+@Injectable()
+export class SignupService {
+ private readonly logger = new Logger(SignupService.name);
+
+ signup(email: string): { message: string } {
+ this.logger.log(`Form submission: ${email}`);
+
+ return {
+ message: "Hello world",
+ };
+ }
+}
+
+// This would normally go in your controller file e.g.
+// src/signup/signup.controller.ts
+@Controller("signup")
+export class SignupController {
+ private readonly logger = new Logger(SignupController.name);
+
+ constructor(
+ private readonly signupService: SignupService,
+ @Inject(ARCJET) private readonly arcjet: ArcjetNest,
+ ) {}
+
+ // Implement a form handler following
+ // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't
+ // compatible with the NestJS Fastify adapter.
+ @Post()
+ @UseInterceptors(NoFilesInterceptor())
+ async index(@Req() req: Request, @Body() body: SignupDto) {
+ const decision = await this.arcjet
+ .withRule(
+ validateEmail({
+ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
+ // block disposable, invalid, and email addresses with no MX records
+ block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"],
+ }),
+ )
+ .protect(req, { email: body.email });
+
+ this.logger.log(`Arcjet: id = ${decision.id}`);
+ this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
+
+ if (decision.isDenied()) {
+ if (decision.reason.isEmail()) {
+ this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`);
+
+ let message: string;
+
+ // These are specific errors to help the user, but will also reveal the
+ // validation to a spammer.
+ if (decision.reason.emailTypes.includes("INVALID")) {
+ message = "email address format is invalid. Is there a typo?";
+ } else if (decision.reason.emailTypes.includes("DISPOSABLE")) {
+ message = "we do not allow disposable email addresses.";
+ } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) {
+ message =
+ "your email domain does not have an MX record. Is there a typo?";
+ } else {
+ // This is a catch all, but the above should be exhaustive based on the
+ // configured rules.
+ message = "invalid email.";
+ }
+
+ throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST);
+ } else {
+ throw new HttpException("Forbidden", HttpStatus.FORBIDDEN);
+ }
+ } else if (decision.isErrored()) {
+ if (decision.reason.message.includes("missing User-Agent header")) {
+ // Requests without User-Agent headers can not be identified as any
+ // particular bot and will be marked as an errored decision. Most
+ // legitimate clients always send this header, so we recommend blocking
+ // requests without it.
+ this.logger.warn("User-Agent header is missing");
+ throw new HttpException("Bad request", HttpStatus.BAD_REQUEST);
+ } else {
+ // Fail open to prevent an Arcjet error from blocking all requests. You
+ // may want to fail closed if this controller is very sensitive
+ this.logger.error(`Arcjet error: ${decision.reason.message}`);
+ }
+ }
+
+ return this.signupService.signup(body.email);
+ }
+}
diff --git a/src/snippets/get-started/nest-js/Step3.mdx b/src/snippets/get-started/nest-js/Step3.mdx
index 04d43675..ae72e18c 100644
--- a/src/snippets/get-started/nest-js/Step3.mdx
+++ b/src/snippets/get-started/nest-js/Step3.mdx
@@ -7,12 +7,13 @@ import GlobalGuard from "./GlobalGuard.ts?raw";
Update your `src/main.ts` file with the contents:
{" "}
+
-This creates a global guard that will be applied to all routes. In a real
-application, implementing guards or per-route protections would give you more
-flexibility. See [our example app](https://github.com/arcjet/example-nestjs) for
-how to do this.
+This creates a global [guard](https://docs.nestjs.com/guards) that will be
+applied to all routes. In a real application, implementing guards or per-route
+protections would give you more flexibility. See [our example
+app](https://github.com/arcjet/example-nestjs) for how to do this.