Skip to Content

Route Subscribers

Route subscribers intercept operations at the controller level, providing access to the HTTP request context including headers, IP address, and authenticated user information.

Creating a Route Subscriber

Extend ApiRouteSubscriberBase and decorate with @ApiRouteSubscriber:

post-audit.subscriber.ts
import { Injectable } from "@nestjs/common"; import { ApiRouteSubscriber, ApiRouteSubscriberBase, TApiSubscriberRouteBeforeCreateContext, TApiSubscriberRouteAfterCreateContext } from "@elsikora/nestjs-crud-automator"; import { PostEntity } from "./post.entity"; @Injectable() @ApiRouteSubscriber({ entity: PostEntity, priority: 10, // Higher priority executes first }) export class PostAuditSubscriber extends ApiRouteSubscriberBase<PostEntity> { // Implement hook methods using helper types }

Available Hooks

Before Hooks

Called before the main operation:

onBeforeCreate

async onBeforeCreate( context: TApiSubscriberRouteBeforeCreateContext<PostEntity> ): Promise<TApiSubscriberRouteBeforeCreateContext<PostEntity>["result"] | undefined> { const { body } = context.result; // Fully typed access to route metadata const method = context.DATA.method; const entityMetadata = context.DATA.entityMetadata; // Before hooks have route metadata and optional authorizationDecision in DATA // Request payload lives in context.result (body/parameters/query) return context.result; }

onBeforeUpdate

async onBeforeUpdate( context: TApiSubscriberRouteBeforeUpdateContext<PostEntity> ): Promise<TApiSubscriberRouteBeforeUpdateContext<PostEntity>["result"] | undefined> { const { body } = context.result; // Track last modified body.lastModifiedAt = new Date(); return context.result; }

onBeforePartialUpdate

Same payload as onBeforeUpdate (parameters + partial body). Use it to intercept PATCH routes.

onBeforeGet, onBeforeGetList, onBeforeDelete

async onBeforeGet( context: TApiSubscriberRouteBeforeGetContext<PostEntity> ): Promise<TApiSubscriberRouteBeforeGetContext<PostEntity>["result"] | undefined> { // Log access with route metadata console.log("Accessing post via route:", context.DATA.methodName, context.result.parameters); return context.result; } async onBeforeDelete( context: TApiSubscriberRouteBeforeDeleteContext<PostEntity> ): Promise<TApiSubscriberRouteBeforeDeleteContext<PostEntity>["result"] | undefined> { // Validation logic before deletion console.log("Attempting to delete post:", context.result.parameters); return context.result; }

After Hooks

Called after successful operation. After hooks have access to extended DATA with authentication, authorizationDecision, headers, IP, and the last request payload (body/parameters/query):

onAfterCreate

async onAfterCreate( context: TApiSubscriberRouteAfterCreateContext<PostEntity> ): Promise<PostEntity | undefined> { const post = context.result; // Access authenticated user (available in after hooks) const user = context.DATA.authenticationRequest?.user; const clientIp = context.DATA.ip; const userAgent = context.DATA.headers["user-agent"]; // Send notification await this.notificationService.send({ to: post.authorId, message: `Your post "${post.title}" was created successfully`, }); // Log audit with full context console.log(`User ${user?.id} created post ${post.id} from ${clientIp}`); return post; }

onAfterUpdate

async onAfterUpdate( context: TApiSubscriberRouteAfterUpdateContext<PostEntity> ): Promise<PostEntity | undefined> { const post = context.result; const user = context.DATA.authenticationRequest?.user; // Invalidate cache await this.cacheService.del(`post:${post.id}`); // Notify followers await this.notifyFollowers(post, user?.id); return post; }

onAfterPartialUpdate

Same payload as onAfterUpdate, but runs for PATCH routes.

onAfterGet, onAfterGetList, onAfterDelete

async onAfterGetList( context: TApiSubscriberRouteAfterGetListContext<PostEntity> ): Promise<IApiGetListResponseResult<PostEntity> | undefined> { const response = context.result; // Fully typed access to extended DATA const user = context.DATA.authenticationRequest?.user; const ip = context.DATA.ip; // Track analytics await this.analyticsService.trackView("post_list", { count: response.items.length, userId: user?.id, ip, }); return response; }

Error Hooks

Called when an operation fails:

onBeforeErrorCreate, onAfterErrorCreate

async onBeforeErrorCreate( context: IApiSubscriberRouteErrorExecutionContext<PostEntity, IApiSubscriberRouteExecutionContextData<PostEntity>>, error: Error ): Promise<void> { // Log before error propagates console.error("Post creation is about to fail:", { error: error.message, data: context.DATA, }); } async onAfterErrorCreate( context: IApiSubscriberRouteErrorExecutionContext<PostEntity, IApiSubscriberRouteExecutionContextDataExtended<PostEntity, PostEntity>>, error: Error ): Promise<void> { // Cleanup or notify after error await this.notificationService.notifyAdmins({ type: "post_creation_failed", error: error.message, user: context.DATA.authenticationRequest?.user, }); }

Other Error Hooks

async onBeforeErrorUpdate( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onAfterErrorUpdate( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onBeforeErrorPartialUpdate( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onAfterErrorPartialUpdate( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onBeforeErrorGet( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onAfterErrorGet( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onBeforeErrorGetList( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onAfterErrorGetList( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onBeforeErrorDelete( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ } async onAfterErrorDelete( context: IApiSubscriberRouteErrorExecutionContext<PostEntity>, error: Error ): Promise<void> { /* ... */ }

Accessing Request Data

Route subscribers have different DATA types for before and after hooks:

Before Hooks DATA

async onBeforeCreate( context: TApiSubscriberRouteBeforeCreateContext<PostEntity> ): Promise<TApiSubscriberRouteBeforeCreateContext<PostEntity>["result"] | undefined> { // Base route metadata (+ optional authorizationDecision) const method = context.DATA.method; const methodName = context.DATA.methodName; const entityMetadata = context.DATA.entityMetadata; const properties = context.DATA.properties; return context.result; }

After Hooks DATA

async onAfterCreate( context: TApiSubscriberRouteAfterCreateContext<PostEntity> ): Promise<PostEntity | undefined> { // Extended DATA with request context const user = context.DATA.authenticationRequest?.user; const contentType = context.DATA.headers["content-type"]; const userAgent = context.DATA.headers["user-agent"]; const clientIp = context.DATA.ip; // Plus all base metadata const method = context.DATA.method; const entityMetadata = context.DATA.entityMetadata; return context.result; }

Practical Examples

IP-Based Access Control

Note: IP address is available in extended DATA for after/error hooks. For before hooks, read it from the request payload (context.result) if you type it explicitly.

ip-audit.subscriber.ts
@Injectable() @ApiRouteSubscriber({ entity: PostEntity, priority: 100 }) export class IpAuditSubscriber extends ApiRouteSubscriberBase<PostEntity> { async onAfterCreate(context: TApiSubscriberRouteAfterCreateContext<PostEntity>): Promise<PostEntity | undefined> { const clientIp = context.DATA.ip; const user = context.DATA.authenticationRequest?.user; // Log access with IP await this.auditService.log({ action: "create", entityId: context.result.id, userId: user?.id, ip: clientIp, }); return context.result; } }

Rate Limiting Per User

user-rate-limit.subscriber.ts
@Injectable() @ApiRouteSubscriber({ entity: PostEntity, priority: 90 }) export class UserRateLimitSubscriber extends ApiRouteSubscriberBase<PostEntity> { private readonly userLimits = new Map<string, { count: number; resetAt: number }>(); async onBeforeCreate(context: TApiSubscriberRouteBeforeCreateContext<PostEntity>): Promise<TApiSubscriberRouteBeforeCreateContext<PostEntity>["result"] | undefined> { const userId = context.result.authenticationRequest?.user?.id; if (!userId) return context.result; const now = Date.now(); const userLimit = this.userLimits.get(userId); if (userLimit && userLimit.resetAt > now) { if (userLimit.count >= 10) { throw new TooManyRequestsException("Rate limit exceeded"); } userLimit.count++; } else { this.userLimits.set(userId, { count: 1, resetAt: now + 60000, // 1 minute }); } return context.result; } }

Content Moderation

content-moderation.subscriber.ts
@Injectable() @ApiRouteSubscriber({ entity: PostEntity, priority: 80 }) export class ContentModerationSubscriber extends ApiRouteSubscriberBase<PostEntity> { constructor(private readonly moderationService: ModerationService) { super(); } async onBeforeCreate(context: TApiSubscriberRouteBeforeCreateContext<PostEntity>): Promise<TApiSubscriberRouteBeforeCreateContext<PostEntity>["result"] | undefined> { const { body } = context.result; const isInappropriate = await this.moderationService.check(body.content); if (isInappropriate) { throw new BadRequestException("Content contains inappropriate material"); } return context.result; } }

Notification System

post-notification.subscriber.ts
@Injectable() @ApiRouteSubscriber({ entity: PostEntity, priority: 50 }) export class PostNotificationSubscriber extends ApiRouteSubscriberBase<PostEntity> { constructor( private readonly notificationService: NotificationService, private readonly followerService: FollowerService, ) { super(); } async onAfterCreate(context: TApiSubscriberRouteAfterCreateContext<PostEntity>): Promise<PostEntity> { const post = context.result; const author = context.DATA.authenticationRequest?.user; // Notify followers const followers = await this.followerService.getFollowers(author.id); for (const follower of followers) { await this.notificationService.send({ userId: follower.id, type: "new_post", data: { postId: post.id, authorId: author.id }, }); } return post; } async onAfterUpdate(context: TApiSubscriberRouteAfterUpdateContext<PostEntity>): Promise<PostEntity> { const post = context.result; // Notify post author of update await this.notificationService.send({ userId: post.authorId, type: "post_updated", data: { postId: post.id }, }); return post; } }

Priority Ordering

Control execution order with priority:

// High priority (executes first) @ApiRouteSubscriber({ entity: PostEntity, priority: 100 }) export class SecuritySubscriber extends ApiRouteSubscriberBase<PostEntity> {} // Medium priority @ApiRouteSubscriber({ entity: PostEntity, priority: 50 }) export class ValidationSubscriber extends ApiRouteSubscriberBase<PostEntity> {} // Low priority (executes last) @ApiRouteSubscriber({ entity: PostEntity, priority: 10 }) export class LoggingSubscriber extends ApiRouteSubscriberBase<PostEntity> {}

Next Steps

Last updated on