SWASTIJ CONSULTANCY
-
Introduction
In this article, I will show you how to implement RBAC in NestJS using NATS.
We will create a simple application with two roles:
admin
anduser
. The admin role will have full access to the system, while the user role will only be able to view data.But first, let us clear some basics.
Why NATS?
- * It
fits the microservice architecture
. - * lightweight
- * flexible
- * highly performant
- * considering factors like load balancing and service discovery, it makes it easier to create microservices with NATS.
- * Data shows that for the execution of requests sent serially,
NATS takes much less time than HTTP
.
RBAC
Role-based access control (RBAC)
is a method of restricting network access based on the roles of individual users within an enterprise. Organizations use RBACalso called role-based security
to parse access levels based on an employee's roles and responsibilities.Role-based system access (RBAC) is a security mechanism that allows you to control who has access to what resources in your system. With RBAC, you can define roles that have different permissions, and then assign users to those roles. This allows you to fine-grained control over who can access your system and what they can do.
- * It
-
Role-Based System Access in NestJS Using NATS
NestJS
is a framework for building scalable, efficient, and easy-to-maintain web applications. NATS is a lightweight messaging system that can be used to decouple different parts of your application.
Let's create a simple application with two roles: admin and user. The admin role will have full access to the system, while the user role will only be able to view data.
Getting Started
The first step is to create a new NestJS
project. We can do this by running the following command:
nest new project-name
I have used nest-access-control library
to implement RBAC in my microservice architecture.
To implement RBAC in NestJS using NATS, you will need to do the following:
-
* Define your roles and permissions.
-
* Create a user model.
-
* Create a role model.
-
* Create a service to manage roles and permissions.
-
* Create a controller to authenticate users and assign them roles.
Here is an example of how to define roles and permissions:
//app.roles.ts
import { RolesBuilder } from 'nest-access-control/roles-builder.class';
export enum AppRoles {
USER = 'user',
ADMIN = 'admin',
}
export const roles: RolesBuilder = new RolesBuilder();
roles
.grant(AppRoles.USER) // define new or modify existing role. also takes an array.
.createOwn('user')
.readOwn('user')
.grant(AppRoles.ADMIN)
.createAny('user')
.readAny('user')
.readAny('business-info')
.createAny('business-info');
Creating Role Guards and Decorators
Now that we’ve defined our roles and permissions using RolesBuilder
, the next step is to protect specific routes or actions based on the assigned roles. This is where custom decorators and guards come in.
📌 Step 1: Create a @InjectRolesBuilder( ) Decorator
//inject-roles-builder.decorator.ts
import { Inject } from '@nestjs/common';
import { ROLES_BUILDER_TOKEN } from 'nest-access-control/constants';
/**
* Get access to the underlying `RolesBuilder` Object
*/
export const InjectRolesBuilder = () => Inject(ROLES_BUILDER_TOKEN);
🛡️ Step 2: Create a RolesGuard - "ACGuard"
//acguard.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IQueryInfo } from 'accesscontrol';
import { InjectRolesBuilder } from 'src/decorators/inject-roles-builder.decorator';
import { Role } from 'nest-access-control/role.interface';
import { RolesBuilder } from 'nest-access-control/roles-builder.class';
import UserDb from 'src/mock/user';
@Injectable()
export class ACGuard<User extends any = any> implements CanActivate {
constructor(
private readonly reflector: Reflector,
@InjectRolesBuilder() private readonly roleBuilder: RolesBuilder
) {}
protected async getUser(context: ExecutionContext): Promise<User> {
const user = context.switchToRpc().getData().user;
return user;
}
protected async getUserRoles(
context: ExecutionContext
): Promise<string | string[]> {
const user: any = await this.getUser(context);
if (!user) throw new UnauthorizedException();
const userRole = UserDb.find((u) => u.id == user.id).roles;
return userRole;
}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const roles = this.reflector.get<Role[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const userRoles = await this.getUserRoles(context);
const hasRoles = roles.every((role) => {
const queryInfo: IQueryInfo = role;
queryInfo.role = userRoles;
const permission = this.roleBuilder.permission(queryInfo);
return permission.granted;
});
return hasRoles;
}
}
✅ Don’t forget to register this guard globally or apply it using @UseGuards(RolesGuard) on protected routes.
🧵 Wiring It All Together with NATS
With RBAC now in place at the HTTP layer, let’s talk about how it integrates with your microservices via NATS.
In a microservice architecture, the gateway receives the HTTP request and communicates with other services (like auth, user, product) via NATS.
For example, from the gateway, you could do:
// src/gateway/app.controller.ts
@Get('/me')
async getCurrentUser(@Req() req: any, @Response() response: any) {
try {
const res = await lastValueFrom(
this.userClient.send('me', { headers: req.headers }),
);
return response.status(res.code || res.status || 200).json(res);
} catch (e) {
console.log('error', e);
return response.status(e.code || e.status || 500).json(e.message);
}
}
In the user service, you’d have:
// src/users/users.controller.ts
@UseInterceptors(ClassSerializerInterceptor)
@UseGuards(AuthMiddleware)
@MessagePattern('me')
async getCurrentUser(data: any) {
return await this.appService.handleGetCurrentUser(data.user);
}
NOTE:
Alternatively, depending on your requirements and business logic, you can have the ACGuard at the gateway itself.
Option 2 - ACGuard at gateway
In this case, your gateway will be
// src/users/users.controller.ts
@Get('me')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('user', 'admin')
async getMyUser(@Req() req) {
return this.natsClient.send('get-user', req.user.id);
}
and in user service, you'll have
// src/users/users.service.ts
@MessagePattern('get-user')
getUser(@Payload() userId: string) {
return this.userRepository.findOne({ where: { id: userId } });
}
🧪 Testing the Flow
Once your services are up, test the full flow:
-
* Login as a user or admin → receive JWT
-
* Send request to a protected route
-
* Gateway/user-service checks role with RolesGuard
-
* The request is sent via NATS to the appropriate service
-
* Microservice responds and data is returned
You now have a fully functioning, role-based microservice architecture using NestJS and NATS! 🎉