MDX Content: # Designing a Flexible CRUD and Role-Based Middleware System in NestJS
Building CRUD endpoints is straightforward in NestJS, but making them reusable, role-aware, and easy to extend requires some upfront design. This post outlines how to structure a small framework inside your NestJS app to:
items={[
{ content: ["Generate CRUD modules quickly"] },
{ content: ["Attach basic middleware (logging, validation, auth)"] },
{ content: ["Support role-based access control (RBAC)"] },
{ content: ["Allow role–route permissions to be changed without rewriting all controllers"] }
]}
/>
Core idea
We'll separate responsibilities into three layers:
variant="ordered"
items={[
{
title: "Generic CRUD layer",
content: ["A base controller and service that provide create/read/update/delete operations for any entity."]
},
{
title: "Authorization layer (RBAC)",
content: ["Roles, permissions, and guards that check if the current user can access a given route."]
},
{
title: "Configuration layer",
content: ["A central config (or DB-driven config) defining which roles can call which CRUD operations per resource."]
}
]}
/>
This lets you add a new module (e.g., Posts, Products) by wiring it into the generic CRUD and registering its permissions.
Step 1: Define roles and permissions
Start with simple roles:
items={[
{ title: "admin", content: ["full access to all resources"] },
{ title: "manager", content: ["limited write + read"] },
{ title: "user", content: ["mostly read-only, maybe create for some resources"] }
]}
/>
You can model roles using a TypeScript enum and then define a permission model per resource with actions like create, read, update, and delete. A RolePermissionsMap ties each role to an array of resource-specific permissions. This map can initially live in a config file and later be moved to a database for runtime configurability.
Step 2: Create a generic CRUD controller
NestJS supports generic controllers and services well. The idea is to implement a BaseCrudController<T> that uses a generic service to expose common endpoints: create, findAll, findOne, update, and remove. Each method simply delegates to the injected service, which encapsulates the data-access logic.
A typical BaseCrudController has methods decorated with @Post, @Get, @Put, and @Delete, and accepts generic DTO types for creation and updates. The controller constructor receives a service instance and a resourceKey string that identifies the logical resource, such as 'users' or 'posts'.
Concrete resource controllers then extend this base controller instead of re-implementing CRUD boilerplate. For example, a UsersController can extend BaseCrudController<any, any, any>, inject a UsersService, and pass 'users' as the resourceKey. The resourceKey will be crucial later for RBAC checks because it ties a route to a named resource in the permission map.
For more detail on controllers, routes, and providers, the official NestJS documentation on controllers explains decorator usage, route mapping, and dependency injection patterns that underpin this generic CRUD design.
Step 3: Attach middleware and guards – authentication
Before enforcing roles and permissions, every request should be associated with an authenticated user. In NestJS, JWT or session-based auth is common, often implemented via Passport strategies. You can use @UseGuards(AuthGuard('jwt')) on controllers so that incoming requests go through the JWT strategy, which validates tokens and populates request.user.
Adding @UseGuards(AuthGuard('jwt')) to a controller that extends BaseCrudController ensures all CRUD endpoints require a valid token. The user object made available on the request will later be used by RBAC logic to evaluate roles.
Step 3: Attach middleware and guards – role guard with resource awareness
Next, introduce a RoleGuard that understands which CRUD action a route represents and which resource it targets. One effective pattern is to define a custom metadata decorator, CrudActionMeta, that tags each CRUD handler (create, read, update, delete) with an action string. This metadata is later read via Reflector in the guard.
Each method in BaseCrudController (e.g., create, findAll) is decorated with CrudActionMeta('create'), CrudActionMeta('read'), and so on. The RoleGuard then:
variant="ordered"
items={[
{
content: "Reads the action from metadata."
},
{
content: "Derives the resource key from the controller (via a `resourceKey` field or the class name)."
},
{
content: "Reads user roles from `request.user`."
},
{
content: "Checks a central `RolePermissionsMap` to see if any of the user's roles grant the given action on that resource."
}
]}
/>
If no permission exists, the guard throws ForbiddenException, blocking access.
Step 3 (continued): Implementing and applying the RoleGuard
The RoleGuard is implemented as a standard NestJS guard (implements CanActivate). It injects Reflector to read metadata set by CrudActionMeta. The guard retrieves the CRUD action, and if none is present it may allow the request (e.g., for public or differently protected routes). Then it obtains the HTTP request, reads request.user, and calculates the resource key from the controller class.
The permission check iterates over the roles attached to the user, uses each role to look up its permissions in a RolePermissionsMap, and verifies whether any permission entry matches both the resource name and the requested action. If no match is found, a ForbiddenException is thrown with a descriptive message; otherwise, canActivate returns true.
To apply this guard across all CRUD endpoints uniformly, you attach @UseGuards(AuthGuard('jwt'), RoleGuard) at the BaseCrudController level. Because concrete controllers extend this base class, every CRUD route will be protected by both JWT authentication and role-based authorization without needing to repeat decorators.
Step 4: Make roles and permissions changeable
To avoid hardcoding permissions permanently, you can externalize the RolePermissionsMap into either config files or a database. A config-driven approach reads a JSON or YAML structure via @nestjs/config, allowing environment-specific or file-based permission changes without code modifications. This is suitable for simpler deployments where an operator edits configuration and restarts the service.
For more dynamic control, a database-driven model is preferable. In that setup, roles, resources, and actions are stored in tables. You then implement a PermissionsService that exposes methods like getPermissionsForRole(role: Role): Promise<ResourcePermission[]>, retrieving and possibly caching permission data. The RoleGuard injects this service instead of referencing a static constant, allowing permissions to change at runtime through admin interfaces.
Performance considerations matter when querying permissions on each request. You might introduce an in-memory cache or use Redis to store role-to-permission mappings, refreshing them periodically or when admin changes occur. NestJS's caching and serialization techniques can help integrate Redis or other cache providers cleanly while keeping your guard's logic unchanged.
Step 5: Basic middleware (logging, validation, error handling) and putting it all together
Beyond authentication and RBAC, a reusable CRUD framework should include shared middleware and infrastructure:
items={[
{
title: "Logging middleware",
content: ["Captures HTTP method, URL, and optionally user identity for each request, aiding observability and auditing."]
},
{
title: "Global validation",
content: ["Using class-validator and class-transformer with Nest's ValidationPipe, you can enforce DTO schemas, strip unknown fields (whitelist: true), and automatically transform inputs to typed DTO instances (transform: true). Applying app.useGlobalPipes in main.ts ensures all controllers benefit from these checks."]
},
{
title: "Error filters",
content: ["Global or scoped exception filters can standardize error responses, map internal errors to user-friendly HTTP codes, and integrate with monitoring tools."]
}
]}
/>
When assembled, the system behaves like a small internal framework:
variant="ordered"
items={[
{
content: ["For a new resource, you define its entity, DTOs, and a standard CRUD service implementing create, findAll, findOne, update, and remove."]
},
{
content: ["You create a controller extending BaseCrudController, inject the resource's service, and pass a distinct resourceKey."]
},
{
content: ["You register permissions for that resourceKey in your config or database via the RolePermissionsMap model."]
}
]}
/>
New modules automatically inherit authentication, RBAC, logging, validation, and error handling. Over time, you can refine this foundation with features like field-level permissions, audit logs, rate limiting, or multi-tenant rules, all without rewriting basic CRUD controllers.
Top comments (0)