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: NestExecutionContextpath: The permission path that was checked (ornullfor 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.
Last updated on