How to Implement Role-Based Access Control in NestJS with MongoDB - Part 1

Amanpreet SinghAmanpreet Singh
7 min read

Introduction

This post is the first in a series titled "Implement Role-Based Access Control in NestJS using MongoDB." In this series, we'll dive into creating an RBAC (Role-Based Access Control) app from scratch using NestJS and MongoDB. Whether you're a beginner or looking to implement role-based access control in your app, follow along and build the app with me!

Authentication & Authorization

Before we get into the details, let's first understand what is Authentication and Authorization.

Authentication is the process of identifying users and validating who they claim to be. One of the most standard and obvious factors in authenticating identity is a password. If the user matches the password credentials, it means the identity is valid, and the system grants access to that user. This entire phase is usually done before authorization.

Authorization on the other hand is the process of giving a user permission to access a specific resource or function.

Prerequisites

To get started, you'll need to have Node.js and npm (Node Package Manager) installed on your machine. If you haven't installed them yet, you can download and install them from the official Node.js website (https://nodejs.org/).

You also need to install NestJS CLI by executing the below command. We will also be using MongoDB via Docker

npm i -g @nestjs/cli

For this article, I am using

  • Node v20.2.0

  • NestJs CLI 10.1.12

  • Docker & Docker Desktop (for GUI)

  • MongoDB

Setting up the NestJS Application

Let's create a new NestJs application in your Projects directory or wherever you prefer like. Run the following command in your terminal window.

nest new

Once you run the above command, you will see something like that displayed in the below image.

Image displaying scaffolding of new nest app

Enter your preferred name of the project and select the package manager you would love to use. I am using npm as a package manager. Installation of all the dependencies and scaffolding of your app will be done in a few seconds.

To get started with your project run the following commands:

cd role-base-app # or the name of your project
npm run start

Once your application is started it will show something like this in your terminal.

Terminal image when you run the command

Creating Resources

Throughout the lifespan of any project, we often need to add resources to our project. These resources typically require multiple, repetitive operations that we have to repeat each time we define a new resource.

To create a new resource, simply run the following command in the root directory of your project:

nest g resource users

nest g resource command not only generates all the NestJs building blocks like module, service, and controller classes but also generates entity class, DTO classes as well as the testing (.spec) files.

Also, it automatically creates placeholders for all the CRUD endpoints (routes for REST APIs). Indeed a very very useful command to speed up our development process. We will create a users resource for our application which will include all the roles.

Once you run the above command, use REST API as an option for the transport layer and enter Yes to generate CRUD entry points which automatically create placeholders for all the CRUD endpoints i.e. in our case the routes for REST APIs.

A command line interface displaying the execution of the command `nest g resource users` in a project named "role-base-app." It shows prompts for transport layer and CRUD entry points, and the successful creation of multiple TypeScript files, including controllers, modules, services, DTOs, and entity files, with packages installed successfully.

MongoDB and Mongoose Setup

We will be using docker to set up MongoDB for our application. Let's create the docker-compose file in our project's root directory by running the following command.

touch docker-compose.yml

Before we add anything to the compose file, let's create an environment file .env to store DB details and other secret credentials which we do not want to be publically available. The file should be created at the root of the project folder.

touch .env

Add the database contents to your .env file. Later we are going to use this file to add more secret variables such as JWT information, etc.

# DB
MONGODB_NAME= role-base
MONGODB_URI= mongodb://localhost:27017/role-base

Now, let's update the docker-compose file, make sure to add the following content in that file.

# version is now obsolete
version: "3" 

services:
  db:
    image: mongo # container image to be used
    restart: always 
    ports: # expose ports in “host:container” format
      - 27017:27017
    environment: #env variables to pass into the container
       MONGODB_DATABASE: ${MONGODB_NAME} # DB name as per the environment file

Now let's start our container in background or detached mode by running the following command

docker compose up -d

Initially, it will pull all the images if not present, in our case the image mongo will be pulled from the docker hub.

Terminal output showing Docker compose up command pulling 10 images, all layers are successfully pulled and the role-base-app_default network is created, with the role-base-app-db-1 container started.

Introducing the Mongoose Module

Mongoose is the most popular MongoDB object modelling tool. We can install Mongoose dependencies in our application by running the following command

npm i mongoose @nestjs/mongoose

Once dependencies are installed let's add MongooseModule in our AppModule

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        uri: configService.get<string>('MONGODB_URI'),
      }),
      inject: [ConfigService],
    }),
    UsersModule
  ],
})

In the above code, the Mongoose module is configured asynchronously using MongooseModule.forRootAsync({}) method.

Inside the object, we can see the imports property, which is an array of modules that need to be imported before configuring the Mongoose module. In this case, it imports the ConfigModule, which suggests that the configuration for the Mongoose module might depend on the configuration provided by the ConfigModule.

The useFactory property is a callback function that is responsible for creating the Mongoose module configuration. It is an asynchronous function that takes an instance of the ConfigService as a parameter. The ConfigService is injected into the function using the inject property.

Make sure you have added the @nestjs/config package by executing the below command.

npm i @nestjs/config

Next, we start your app in the development mode by running the following command

npm run start:dev

If everything works fine, you will see the MongooseModule initialized which means we have successfully connected with our MongoDB database.

In case of any issues please make sure your container is up and running and also the DB name matches what you have in the docker-compose.yml file.

Terminal output showing the initialization of a NestJS application with various modules and routes being mapped. The log indicates no errors and displays timestamps, log levels, and messages related to the application's startup process.

Creating a Mongoose Model

One of the most vital concepts in MongoDB is the idea of "Data Models". These Models are responsible for creating, reading, and deleting "documents" from the Mongo database.

Every schema we create maps to our MongoDB collection and defines the shape of the documents in that collection.

Let's head over to our user.entity.ts file and add the Schema definition for the User model. We use a @Schema() decorator to define a schema for the User class.

The class named User will consist of name, email, password and role as properties for now and to define a property in schema we use @Prop() decorator.

Lastly the SchemaFactory.createForClass() method is used to create a schema for the User class and exported via UserSchema constant.

import { Prop, Schema } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class User extends Document{
  @Prop()
  name: string;

  @Prop()
  email: string;

  @Prop()
  password: string;

  @Prop()
  role: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

Now, let's make Mongoose aware of this module by updating our UserModule is as follows.


@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: User.name,
        schema: UserSchema,
      },
    ]),
  ],
...
...
})

The MongooseModule.forFeature() method is used to define a feature module for the User entity. The forFeature() method takes an array of objects that define the name of the entity and the schema that should be used for the entity.

In this case, the name property is set to User.name, which is the name of the User class defined in another file. The schema property is set to UserSchema, which is the schema for the User entity.

Conclusion

That's it for today. We have successfully set up a new NestJs app and created a Users resource with database configuration and integration of MongoDB and Mongoose as the ORM. We also added environment DB variables to secure our app.

Next Post

Stay tuned for the next post, where we will deep dive more into Authentication. The new blog post will be published by 10 July 2024.

References

  1. GitHub Repo
1
Subscribe to my newsletter

Read articles from Amanpreet Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amanpreet Singh
Amanpreet Singh

Passionate developer and software crafter with a passion for problem-solving. Always learning and growing in the field. Currently looking for a job or freelance gig. Need a software consultant? I am your guy! With experience in websites, applications, and cloud services, I can provide top-notch solutions for your business needs. Let's work together to take your tech game to the next level. Contact me today for part-time consulting opportunities