Aviary

Getting Started

Install @dudousxd/nestjs-authz, register the module, write your first policy, guard a route, and check abilities programmatically.

This guide takes you from an empty NestJS app to a fully guarded route in five steps. We'll use a small blog domain — User and Post — and end with both declarative enforcement (@Can on a route) and a programmatic check (Gate in a service).

Prerequisites

  • NestJS 10+ (v10 and v11 both work)
  • TypeScript 5+ with experimentalDecorators and emitDecoratorMetadata enabled
  • An authentication layer that attaches the current user to the request — Passport, a JWT guard, sessions, anything. authz authorizes; it does not authenticate.

Install

pnpm add @dudousxd/nestjs-authz

The peer dependencies are the standard NestJS trio — you almost certainly have them already:

pnpm add @nestjs/common @nestjs/core reflect-metadata

@dudousxd/nestjs-context is an optional peer. When it's present, the gate reads the current user for free; when it's absent, you pass the user explicitly with gate.forUser(...). You don't need it to get started.

Define your domain

Nothing special here — these are your ordinary entities. authz never models the user or the resource; it just passes them to your rules.

// src/user.entity.ts
export class User {
  id!: number;
  isAdmin!: boolean;
  verified!: boolean;
}

// src/post.entity.ts
export class Post {
  id!: number;
  authorId!: number;
  published!: boolean;
}

Write your first policy

A policy is a class that groups every ability for one resource. Decorate it with @Policy(Resource); each method is an ability that receives (user, resource) and returns a boolean (or a Promise<boolean>).

// src/post.policy.ts
import { Policy } from '@dudousxd/nestjs-authz';
import { User } from './user.entity.js';
import { Post } from './post.entity.js';

@Policy(Post)
export class PostPolicy {
  // Optional bypass hook — runs before every ability on this policy.
  before(user: User) {
    if (user.isAdmin) return true; // grant everything; `undefined` falls through
  }

  view(user: User, post: Post) {
    return post.published || post.authorId === user.id;
  }

  update(user: User, post: Post) {
    return post.authorId === user.id;
  }

  create(user: User) {
    return user.verified; // class-level ability — no instance needed
  }
}

@Policy also makes the class an @Injectable() provider for you, so it participates in DI and can be auto-discovered. You don't add @Injectable() yourself. See Policies for the full breakdown.

Register the module

Import AuthzModule.forRoot() once, at the root of your app. Either list your policies explicitly or rely on @Policy auto-discovery (both work — see Policies).

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthzModule } from '@dudousxd/nestjs-authz';
import { PostPolicy } from './post.policy.js';
import { PostsController } from './posts.controller.js';

@Module({
  imports: [
    AuthzModule.forRoot({
      policies: [PostPolicy],       // or rely on @Policy auto-discovery
      superAdmin: (u) => u.isAdmin, // global before-hook (see the caveat below)
    }),
  ],
  controllers: [PostsController],
})
export class AppModule {}

forRoot registers a global module that wires up the Gate, the CanGuard and RolesGuard (both as APP_GUARD), the PolicyRegistry, and a default resource resolver — so the next step works out of the box.

superAdmin: (u) => u.isAdmin reads u.isAdmin — but on the context path the gate hands these hooks a raw UserRef ({ type, id }), not your hydrated User. Until you configure resolveUser, that check won't see isAdmin. This is the single most common authz gotcha — read Current user before relying on entity fields.

Guard a route, and check programmatically

Declarative — annotate the route with @Can. The CanGuard (already registered) resolves the Post and runs PostPolicy.update(currentUser, post). A denial throws ForbiddenException (HTTP 403) automatically.

// src/posts.controller.ts
import { Controller, Patch, Post as HttpPost, Param } from '@nestjs/common';
import { Can } from '@dudousxd/nestjs-authz';
import { Post } from './post.entity.js';

@Controller('posts')
export class PostsController {
  @Patch(':id')
  @Can('update', Post) // resolves Post by :id, runs PostPolicy.update(currentUser, post)
  update(@Param('id') id: string) {
    /* ... only runs if authorized ... */
  }

  @HttpPost()
  @Can('create', Post, { classLevel: true }) // runs PostPolicy.create(currentUser) — no instance loaded
  create() {
    /* ... */
  }
}

Programmatic — inject the Gate and check abilities inside a service:

import { Injectable } from '@nestjs/common';
import { Gate } from '@dudousxd/nestjs-authz';
import { Post } from './post.entity.js';

@Injectable()
export class PostsService {
  constructor(private readonly gate: Gate) {}

  async publish(post: Post) {
    await this.gate.authorize('update', post); // throws ForbiddenException on deny
    // ... safe to mutate ...
  }

  async canDelete(post: Post) {
    return this.gate.allows('delete', post); // returns boolean, never throws
  }
}

Both forms read the current user from nestjs-context automatically. When you don't have a context (or want to check a different user), bind one explicitly:

await this.gate.forUser(someUser).authorize('update', post);

What you've built

  • A PostPolicy whose methods are your authorization rules.
  • A globally registered AuthzModule that auto-protects @Can-annotated routes.
  • Two enforcement styles: @Can at the edge, Gate in services.

Next steps

On this page