Library / SDK
nestjsx/nest-access-control avatar
nestjsx/nest-access-control

nest-access-control puts a grants model behind NestJS decorators

Role and Attribute based Access Control for Nestjs 🔐

1,159 stars80 forksTypeScriptMIT

At a glance

What is it?
nest-access-control is a NestJS module that wraps the accesscontrol library, so a Nest application can declare roles, attach a guard to a route and let a decorator state which resource and action the caller needs. The policy lives in one chainable roles file, and roles can be loaded asynchronously from a database.
Who is it for?
nest-access-control is worth adopting if your Nest application already has a role concept and you want the check on the route rather than scattered through the service layer, because the roles file is a single readable artefact and the guard throws a ForbiddenException rather than returning something falsy.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A module over an existing grants library, not a new policy engine

nest-access-control is described as a helper module for building a role and attribute based access control system for NestJS, and the key architectural fact is in the README's second line: this module is built on top of onury's accesscontrol library.

That matters for what you are adopting. The policy semantics, the chainable query API, the attribute glob notation and the error behaviour all come from accesscontrol. What this project contributes is the NestJS integration: a module you import, a guard, decorators, and an async factory for roles that come from a database.

The motivation the README gives is candid. The author's system needed a control panel, so that everything could be controlled and monitored from one place, and it really needed a role based access control system, so the author built this module and shared it, with pull requests welcome.

The feature list is inherited from the library and worth reading because it defines what the guard can and cannot express. There is a chainable friendly API, for example ac.can(role).create(resource). There is role hierarchical inheritance, so a role can extend another. Grants can be defined at once, for example from a database result, or one by one. Permissions are granted or denied by attributes defined with glob notation, with nested object support. You can filter a data model instance by the allowed attributes. You can control access on own or any resources, which is the possession distinction. You can lock the underlying grants model. There are no silent errors, and the grants are stored in memory, so there are no database queries, which is where the speed claim comes from.

The audience is a NestJS team that has already decided on roles and wants the enforcement point to be the route decorator.

The policy is one chainable roles file

The example in the README is a video service where users share videos and some admins control them, and it starts with the roles file.

You define your roles as an enum, then build them with the RolesBuilder class, which extends the AccessControl class from the accesscontrol package:

ts
// app.roles.ts

export enum AppRoles {
  USER_CREATE_ANY_VIDEO = 'USER_CREATE_ANY_VIDEO',
  ADMIN_UPDATE_OWN_VIDEO = 'ADMIN_UPDATE_OWN_VIDEO',
}

export const roles: RolesBuilder = new RolesBuilder();

roles
  .grant(AppRoles.USER_CREATE_ANY_VIDEO)
    .createOwn('video')
    .deleteOwn('video')
    .readAny('video')
  .grant(AppRoles.ADMIN_UPDATE_OWN_VIDEO)
    .extend(AppRoles.USER_CREATE_ANY_VIDEO)
    .updateAny('video', ['title'])
    .deleteAny('video');

Three things are happening in that chain. The verbs are the actions, create, delete, read, update. The target of the action is the resource, video, and the possession is either own or any, so createOwn and readAny. And the attributes are the second argument, so updateAny('video', ['title']) grants the ability to update only the title attribute, while the createOwn call with no attribute list is documented as equivalent to createOwn('video', ['*']).

The extend call is the inheritance mechanism, and the comment notes that switching to another role does not break the chain, and that the calls also take an array.

There is even a pro tip about keeping all roles organised in one file, and the example file is called app.roles.ts. That is a small thing, and it is the right advice, because a grants model that is scattered across controllers is a grants model nobody can audit.

Three decorators, and a guard that throws

The module introduces three decorators, and the README says there were two new decorators before admitting there are three.

The one you will use most is UseRoles, which takes an object describing the resource, the action and the possession, and defines what roles the user must have to access the route. It may take one role or several, and the rule is that all of the roles must be satisfied, which is an and rather than an or and is worth knowing before you rely on it.

The second is UserRoles, which returns the user's roles from the request object so you can check permissions manually instead of letting the guard do it. It takes an optional property key, so UserRoles('permissions') reads the roles from that property of the user object rather than from the default location.

The third is the guard, ACGuard, which the README introduces with a joke about the name. The guard checks the user's roles, and if they have permission to access the resource it returns true, and otherwise it throws a ForbiddenException.

That last detail is the one that matters in practice. Throwing a ForbiddenException means the framework's exception filter produces a proper 403 response, rather than the guard returning false and the framework producing a 403 with a generic message, or your own code checking a boolean and forgetting to. In the example the controller composes it with an existing authentication guard:

ts
@Controller()
export class AppController  {
  constructor(private readonly appService: AppService)  {}
  @UseGuards(AuthGuard, ACGuard)
  @UseRoles({
    resource:  'video',
    action:  'read',
    possession:  'any',
  })
  @Get()
  root(@UserRoles() userRoles: any)  {
    return this.appService.root(userRoles);
  }
}

So the pattern is: authenticate with your own guard, then authorise with ACGuard, and read the roles off the request if the handler needs them. The roles structure itself is documented in the roles.interface.ts file and in the original accesscontrol documentation.

forRootAsync loads roles from a database, with an import trap

The asynchronous factory exists for the case where the roles live in a database, and the README's example is a RoleProvider that returns a promise of role names.

ts
@Module({
  imports: [
    AccessControlModule.forRootAsync({
      imports: [TestModule],
      inject: [RoleService],
      useFactory: async (roleService: RoleService): Promise<RolesBuilder> => {
        return new RolesBuilder(await roleService.getRoles());
      },
    }),
  ],
})
export class AccessModule {}

The important part is the warning that follows the example. The use of imports in the forRootAsync method is what allows you to inject exported providers from the imported module, and injecting providers provided in the same module as the imported AccessControlModule will result in the provider not being found, because the module is created before the providers.

That is a NestJS module resolution detail, and it is the kind of thing that costs an afternoon if you hit it without the explanation. The fix is to put the provider in a separate module, export it, and list that module in the imports array of the forRootAsync call.

There is also a second, simpler registration path for the static case, used in the main example, where the roles file is imported and passed straight in:

ts
import { roles } from './app.roles';

@Module({
  imports: [AccessControlModule.forRoles(roles)],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

The difference between forRoles and forRootAsync is the whole design decision of the module in one line: static roles live in your repository next to your code, dynamic roles come from a database at bootstrap.

The module is deliberately thin, and the limits are in the grants model

Three things the module gives you beyond the raw library are listed in the README: it is decorator based, so most of the time you use decorators in your routes; there is a built-in ACGuard so you can use it directly; and you get access to the underlying AccessControl object from everywhere, which is how you do the programmatic checks that the decorator cannot express, such as the can(role).create(resource) chain or the ability to filter a data model instance by the attributes a role is allowed to see.

Those three are exactly the right three, because they cover the two ways people enforce permissions: at the route, and in the code that touches the data. The second one is the one that is easy to forget, and a module that hands you the underlying object makes the programmatic path a first class option rather than something you reimplement.

The limits follow from the model rather than from the wrapper. Permissions are expressed as roles, actions, resources, possession and attributes. A check that depends on the specific row, the current tenant, the time of day or a field value that is not part of the role definition does not fit, and the library's answer is the attribute mechanism, which is about which fields of a resource a role may touch rather than about which resources a given user may touch. If your application needs per user or per record grants, a different model fits better.

Two more properties from the inherited feature list are worth naming because they are decisions rather than defaults. The ability to lock the underlying grants model means you can prevent code from mutating the policy after startup, which turns a runtime check into a startup time decision. And the promise of no silent errors means a malformed grant fails loudly, which is what you want in an authorisation layer and is not something every implementation gets right.

The build tooling dates from the Nest 10 era

The repository is a TypeScript project with a conventional layout: src for the sources, lib for the build output, an example directory with a full Nest application including its own nest-cli.json and eslint and prettier configuration, a gulpfile.js for the build, a jest.json for the test runner configuration, and a tslint.json.

The build is gulp based, with the build script running gulp build and then gulp move, and a build:lib variant that builds into the lib directory and copies package.json and README.md alongside it, and a publish step that changes into lib and publishes from there. That copy pattern is how the package ends up with its own package.json in the published directory, which is an older but still common layout for a library that ships compiled JavaScript and its own metadata.

The dependency versions date the project. The Nest packages in devDependencies are at 10.1.3, jest is 29.6.2, typescript is 5.1.6, ts-jest is 29.1.1, and the linting is tslint rather than eslint. The test commands are jest with the jest.json configuration, and there are separate scripts for watch mode and for coverage into a coverage directory, with coveralls in the dev dependencies for the reporting side.

The runtime dependencies are tiny by design: accesscontrol at 2.2.1 and tslib, and nothing else. The only peer dependency is @nestjs/graphql, accepted across versions 7 through 13 and marked optional in peerDependenciesMeta, which tells you the GraphQL integration is a convenience rather than a requirement, and that the module is usable in a plain REST application without installing anything extra.

The licence is MIT, the repository is not archived, and the last push was on 2026-10-17.

Editorial conclusion

nest-access-control is worth adopting if your Nest application already has a role concept and you want the check on the route rather than scattered through the service layer, because the roles file is a single readable artefact and the guard throws a ForbiddenException rather than returning something falsy. It is the wrong choice if your permissions change per row, per tenant or per request attribute, since the grants model is role shaped, and it is the wrong choice if you need an audit trail of decisions, which this module does not provide. Before you adopt it, read the peer dependency list, because @nestjs/graphql is an optional peer and the rest of the module is deliberately thin, and note that the package version is 3.2.0 with no GitHub releases and the repository pushed to on 2026-10-17.

Frequently asked questions

How do I install nest-access-control?

With npm install nest-access-control --save, or yarn add nest-access-control. The package is nest-access-control, and it only depends on accesscontrol and tslib at runtime.

How do I define roles in nest-access-control?

With the RolesBuilder class, which extends AccessControl from the accesscontrol package. You grant a role and chain actions such as createOwn('video'), readAny('video') or updateAny('video', ['title']), and you can call extend to inherit another role's capabilities. The roles are conventionally kept in one file such as app.roles.ts.

What do the decorators in nest-access-control do?

UseRoles states which resource, action and possession a route needs, and all listed roles must be satisfied. UserRoles returns the user's roles from the request, with an optional property key to read them from a different field on the user object. ACGuard is the guard that checks those roles and throws a ForbiddenException when access is denied.

How do I load roles from a database in nest-access-control?

Use AccessControlModule.forRootAsync with imports, inject and useFactory, returning a new RolesBuilder built from the role names the factory resolves. The documentation warns that providers must come from a separate imported module, because providers declared in the same module as the imported AccessControlModule will not be found.

What are the limitations of the grants model nest-access-control uses?

The model is role shaped, with actions, resources, own or any possession and attributes, so per user or per record grants do not fit it. The inherited feature list also notes that permissions are stored in memory with no database queries, and that there is an option to filter a data model instance by the attributes a role may see.

Official sources

  1. Issues
  2. License: MIT
  3. nestjsx/nest-access-control on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nestjsx-nest-access-control.svg)](https://hysenlabs.com/projects/nestjsx-nest-access-control)

Was this useful?