Permix

NestJS

Learn how to use Permix with NestJS

Overview

Set up permissions per request with a guard, enforce them with a @Check decorator. Create the factory with createPermix from permix/nest.

Before getting started with the NestJS integration, make sure you've completed the initial setup steps in the Quick Start guide.

Setup

Create a factory and register its guard as a global APP_GUARD.

import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import { createPermix } from 'permix/nest'

interface Post {
  id: string
  authorId: string
  title: string
  content: string
}

export const permix = createPermix<{
  post: [
    { name: 'create'; type: Post },
    { name: 'read'; type: Post },
    { name: 'update'; type: Post },
  ]
}>()

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useValue: permix.guard(({ req }) => {
        // You can access req.user or other properties to determine permissions
        return {
          post: {
            create: true,
            read: true,
            update: false,
          },
        }
      }),
    },
  ],
})
export class AppModule {}

permix.guard(...) sets up the instance, @Check enforces it. A @Check with no guard on the request throws PermixNotFoundError — it never passes unchecked. Works with the Express and Fastify adapters; non-HTTP contexts (RPC, WebSockets, GraphQL) are skipped.

Registering the guard per route instead of globally? Put @UseGuards above @Check so it runs first:

@Get()
@UseGuards(permix.guard(rules))
@permix.Check('post.read')
findAll() {}

req is untyped inside the guard callback and onForbidden, since Nest's HTTP adapters share no request type. Annotate your own @Req() parameters:

import type { Request } from 'express'

@Get()
findAll(@Req() req: Request) {
  return permix.getOrThrow(req).check('post.read')
}

Checking Permissions

Use @Check on a handler or controller:

import { Controller, Delete, Get, Post, Put } from '@nestjs/common'
import { permix } from './permix'

@Controller('posts')
export class PostsController {
  @Post()
  @permix.Check('post.create')
  create() {
    return { success: true }
  }

  @Put(':id')
  @permix.Check((c) => c('post.read') && c('post.update'))
  update() {
    return { success: true }
  }

  @Delete(':id')
  @permix.Check('post.~all')
  remove() {
    return { success: true }
  }

  @Get()
  @permix.Check('post.~any')
  findAll() {
    return { posts: getAllPosts() }
  }
}

Accessing Permix Directly

Access the instance in a handler with getOrThrow:

@Get()
findAll(@Req() req: Request) {
  const { check } = permix.getOrThrow(req)

  if (check('post.read')) {
    return { posts: getAllPosts() }
  }

  throw new ForbiddenException({
    error: 'You do not have permission to read posts',
  })
}

Entity (ReBAC) checks run here, after the resource is loaded:

@Put(':id')
async update(@Param('id') id: string, @Req() req: Request) {
  const post = await getPostById(id)
  const { check } = permix.getOrThrow(req)

  if (!check('post.update', post)) {
    throw new ForbiddenException({ error: 'You cannot update this post' })
  }

  return { success: true }
}

Using Templates

Reusable rule sets:

const adminTemplate = permix.template({
  post: {
    create: true,
    read: true,
    update: true,
  },
})

{
  provide: APP_GUARD,
  useValue: permix.guard(({ req }) => {
    if (req.user?.role === 'admin') {
      return adminTemplate()
    }

    return {
      post: {
        create: false,
        read: true,
        update: false,
      },
    }
  }),
}

Custom Error Handling

A denied @Check throws ForbiddenException with { error: 'Forbidden' }. Override it with onForbidden:

onForbidden must throw. Unlike the Express integration, do not write to the response directly — returning normally lets Nest raise its own ForbiddenException on top of whatever you sent.

const permix = createPermix<Definition>({
  onForbidden: ({ path }) => {
    throw new ForbiddenException({
      error: `You don't have permission for ${path}`,
    })
  },
})

The onForbidden handler receives:

  • req: The HTTP request object (Express or Fastify)
  • context: Nest ExecutionContext
  • path: The permission path that was checked (or null for callback checks)
  • data: Optional entity data passed to the check

Advanced Usage

Async Permission Rules

The guard callback can be async:

permix.guard(async ({ req }) => {
  const userPermissions = await getUserPermissions(req.user.id)

  return {
    post: {
      create: userPermissions.canCreatePosts,
      read: userPermissions.canReadPosts,
      update: userPermissions.canUpdatePosts,
    },
  }
})

Global guards run in registration order. Register your auth guard before the Permix guard, or req.user is still undefined when the callback runs.

Hooks

Factory-level hooks fire for every request:

permix.hook('check', ({ path, data }) => {
  console.log(`Permission checked: ${path}`, data)
})

Example

Full example: examples/nest.

Edit on GitHub

Last updated on

On this page