NestJS
Updated
5 min read

How to secure your NestJS application with Arcjet

A tutorial for protecting your NestJS application with Arcjet. Arcjet makes it easy to build rate limiting, bot detection, email verification & attack detection into NestJS.

How to secure your NestJS application with Arcjet

Arcjet makes it easy to build rate limiting, bot detection, email verification & attack detection into your application. We started with security SDKs for Next.js and Node.js, but that's no reason you can't use it with other frameworks right away.

This is the first in a multi-part series of blog posts to help you secure various web application frameworks, starting with NestJS.

In this post, we'll discuss the necessary configuration, set up a custom Arcjet guard, and apply it to secure NestJS routes effectively. This integration not only boosts security but also streamlines the management of security policies across your application.

tl;dr: You can download the Arcjet on NestJS example code and start playing right away!

CommonJS versus ECMAScript Modules (ESM)

Arcjet's SDKs use ECMAScript Modules in order to support modern runtimes like Deno and Bun which have varying support for CommonJS.

This has the added benefit of supporting asynchronous module loading, native browser support, and better tree shaking to reduce bundle size during the build process. These in turn lead to improved performance, reduced latency, and faster execution times, which are critical for fast, low-latency API requests.

🧠
Tree shaking is a technique used to eliminate dead code — unused parts of your codebase — resulting in smaller, more efficient bundles. This is particularly effective with ESM due to its static structure, allowing build tools to analyze dependencies more accurately.

NestJS itself is designed to support both CommonJS and ECMAScript Modules, but comes configured out of the box for CommonJS. Because of this, we'll need to configure TypeScript and set the appropriate module settings to enable compatibility between the two systems.

For example, with the default NestJS starter code, you would see module set to commonjs in tsconfig.json, and no type defined in package.json. Starting the NestJS app with npm start would result in the following error:

Error [ERR_REQUIRE_ESM]: require() of ES Module ~/Projects/arcjet-js/arcjet-node/index.js from ~/Projects/arcjet-js/examples/nodejs-nestjs/dist/arcjet/arcjet.guard.js not supported.

Error when using Arcjet with NestJS in CommonJS mode.

To fix this we need to configure NestJS to use ESM.

Configuring NestJS for ESM

There are two files to change for this change to take effect.

In tsconfig.ts, we'll change module to node16 to align your project with the latest stable Node.js module system, and confirm that target is set to ES2021 to ensure that your TypeScript is compiled to a version of JavaScript that supports recent features.

// File: /tsconfig.json

{
    ...
    "module": "node16",
    "target": "ES2021",
    ...
}

In package.json, we'll add type and set it to module to explicitly declare that the project uses ES Modules.

// File: /package.json

{
    ...
    "type": "module",
    ...
}

Setting Up the Configuration Module

NestJS uses modules to organize the application. ConfigModule is particularly important for managing environment variables and other configurations which Arcjet will utilize.

npm install @nestjs/config

Once the package has been installed, we can import ConfigModule in our modules so they have access to the environment variables.

// File: /src/app.module.ts

import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
  controllers: [AppController],
  providers: [AppService, ArcjetGuard],
})

export class AppModule {}

We've also defined ArcjetGuard as a provider, which injects it into the controller to protect that route.

Implementing Arcjet Guard

NestJS Guards intercept incoming requests before they reach the route handler, allowing you to make access control decisions based on specific conditions such as, in this case, the result of calling arcjet.protect(). This provides a flexible and centralized way to enforce security policies within a NestJS application.

We’ll do this by implementing a custom guard named ArcjetGuard. Here’s how to set it up:

// File: /src/arcjet/arcjet.guard.ts

import { CanActivate, ExecutionContext, HttpException, HttpStatus, Inject, Injectable, Optional } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import arcjet from '@arcjet/node';
import { ARCJET_DEFAULT_RULES } from '../config/arcjet.js';

@Injectable()
export class ArcjetGuard implements CanActivate {
    private aj: any;

    constructor(
        private configService: ConfigService,
        @Optional() @Inject('ARCJET_RULES') private rules: any[] = []
    ) {
        // Take the rules defined in config/arcjet.js and overlay them
        // with any additional rules defined in the module file, and
        // instantiate the Arcjet client
        this.aj = arcjet({
            key: this.configService.get<string>('ARCJET_KEY'),
            rules: [...ARCJET_DEFAULT_RULES, ...this.rules],
        });
    }

    // canActivate contains the main logic for determining whether a
    // request should be allowed to proceed. It returns a boolean value
    // (or a Promise that resolves to a boolean) indicating whether the
    // request is authorized.
    async canActivate(
        context: ExecutionContext,
    ): Promise<boolean> {
        const request = context.switchToHttp().getRequest();;

        const decision = await this.aj.protect(request);

        if (decision.isDenied()) {
            if (decision.reason.isRateLimit()) {
                throw new HttpException(
                  "Too many requests",
                  HttpStatus.TOO_MANY_REQUESTS
                );
            }
            if (decision.reason.isBot()) {
                throw new HttpException(
                  "Bot detected",
                  HttpStatus.FORBIDDEN
                );
            }
            throw new HttpException(
              "Forbidden",
              HttpStatus.FORBIDDEN
            );
        }

        return true;
    }
}

Configuring Security Rules for NestJS

A critical part of integrating Arcjet with NestJS is the ability to define and manage security rules effectively. These rules dictate how the Arcjet SDK will handle security concerns like rate limiting, bot detection, and more. Here’s how you can set up these rules:

Default Rules Configuration: Start by creating a /config/arcjet.ts file where you can define a set of default security rules. These are the rules that you want to apply across all requests guarded by Arcjet in your application.

// File: /src/config/arcjet.ts

import { shield } from '@arcjet/node';

export const ARCJET_DEFAULT_RULES = [
    shield({
        mode: "LIVE",
    }),
];

Module-Specific Rules: In larger applications, you may want different parts of your application to have specific security requirements. You can define these custom rules in the NestJS modules where they apply.

Here, the fixedWindow rule is added specifically for ProtectedModule. It imposes a strict rate limit of twenty requests per minute, which is in additional to enabling shield in /src/config/arcjet.ts.

// File: /src/protected/protected.module.ts

...

@Module({
    imports: [ConfigModule.forRoot()],
    controllers: [ProtectedController],
    providers: [
        ProtectedService,

        // Define rate limiting rules for this module, which
        // we want to add to the ArcjetGuard configuration
        // See https://docs.arcjet.com/reference/nodejs#configuration
        // for information on the rules available
        {
            provide: 'ARCJET_RULES',
            useValue: [
                fixedWindow({
                    mode: "LIVE",
                    window: "1m",
                    max: 20,
                }),
            ],
        },

        // Add ArcjetGuard with default configuration
        // overlayed with the preceding additional rules
        ArcjetGuard,
    ],
})

export class ProtectedModule { }

If a fixed window isn't right for you, we also support sliding window and token bucket rate limiting. Different rate limit algorithms are useful in different scenarios, depending on what each part of your application is doing.

What if you want to stop bots from hitting your login page? Arcjet's got you covered there too — simply use the detectBot rule which can be combined with the other protections:

// File: /src/login/login.module.ts

...
        {
            provide: 'ARCJET_RULES',
            useValue: [
                detectBot({
                    mode: "LIVE",
                    block: ["AUTOMATED"],
                }),
                fixedWindow({
                    mode: "LIVE",
                    window: "1m",
                    max: 20,
                }),
            ],
        },

By configuring both default and custom rules, you ensure that the Arcjet SDK can enforce the appropriate level of security based on different parts of your application. This flexibility allows developers to tailor security measures to the specific needs of each NestJS module.

Securing Specific Controllers

Protecting your controller with the ArcjetGuard is then as simple as importing the guard, and using the @UseGuards decorator.

// File: /src/protected/protected.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { ArcjetGuard } from '../arcjet/arcjet.guard.js';
import { ProtectedService } from './protected.service.js';

@Controller('protected')
@UseGuards(ArcjetGuard)
export class ProtectedController {

    constructor(
        private readonly protectedService: ProtectedService,
    ) { }

    @Get()
    getProtected() {
        return this.protectedService.getProtected();
    }

}

Add @UseGuards(ArcjetGuard) to any controller you wish to protect.

Conclusion

Integrating Arcjet with NestJS provides a robust layer of security that is essential for protecting your application from common threats like API abuse and automated bots. This integration not only safeguards your application but also streamlines the management of security policies, allowing for more focused development on other aspects of your application.

Throughout this guide, we’ve covered the necessary steps to configure Arcjet within a NestJS framework, from setting up the TypeScript environment and creating a custom security guard to applying these measures across various parts of the application. We've also discussed the importance of defining security rules that meet the specific needs of different modules and endpoints.

Next Steps

💻
Experiment: Dive into the Arcjet on NestJS example code and tailor it to your specific requirements.
🚀
Test & Deploy: Integrate Arcjet into your real-world NestJS projects, test it locally and check out its effectiveness in production environments.
💬
Share Your Feedback: Join us on the Arcjet Discord server to discuss your experiences, ask questions, and help us refine this integration even further.

Subscribe by email

Get the full posts by email every week.