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,
IApiSubscriberRouteExecutionContext
} 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
}
Available Hooks
Before Hooks
Called before the main operation:
onBeforeCreate
async onBeforeCreate(
context: IApiSubscriberRouteExecutionContext<
PostEntity,
{ body: DeepPartial<PostEntity> }
>
): Promise<{ body: DeepPartial<PostEntity> } | undefined> {
const { body } = context.result;
const user = context.DATA.authenticationRequest?.user;
// Validate or modify body
if (!user) {
throw new UnauthorizedException("User must be authenticated");
}
// Add user ID to body
body.authorId = user.id;
return context.result;
}
onBeforeUpdate
async onBeforeUpdate(
context: IApiSubscriberRouteExecutionContext<
PostEntity,
{ body: DeepPartial<PostEntity> }
>
): Promise<{ body: DeepPartial<PostEntity> } | undefined> {
const { body } = context.result;
// Track last modified
body.lastModifiedAt = new Date();
body.lastModifiedBy = context.DATA.authenticationRequest?.user?.id;
return context.result;
}
onBeforeGet
, onBeforeGetList
, onBeforeGetMany
, onBeforeDelete
async onBeforeGet(
context: IApiSubscriberRouteExecutionContext<PostEntity, PostEntity>
): Promise<PostEntity | undefined> {
// Log access
console.log("User accessing post:", context.ENTITY.id);
return context.result;
}
async onBeforeDelete(
context: IApiSubscriberRouteExecutionContext<PostEntity, PostEntity>
): Promise<PostEntity | undefined> {
const user = context.DATA.authenticationRequest?.user;
// Check ownership before deletion
if (context.ENTITY.authorId !== user?.id && user?.role !== "admin") {
throw new ForbiddenException("You can only delete your own posts");
}
return context.result;
}
After Hooks
Called after successful operation:
onAfterCreate
async onAfterCreate(
context: IApiSubscriberRouteExecutionContext<PostEntity, PostEntity>
): Promise<PostEntity | undefined> {
const post = context.result;
const user = context.DATA.authenticationRequest?.user;
// Send notification
await this.notificationService.send({
to: post.authorId,
message: `Your post "${post.title}" was created successfully`,
});
// Log audit
console.log(`User ${user?.id} created post ${post.id}`);
return post;
}
onAfterUpdate
async onAfterUpdate(
context: IApiSubscriberRouteExecutionContext<PostEntity, PostEntity>
): Promise<PostEntity | undefined> {
const post = context.result;
// Invalidate cache
await this.cacheService.del(`post:${post.id}`);
// Notify followers
await this.notifyFollowers(post);
return post;
}
onAfterGet
, onAfterGetList
, onAfterGetMany
, onAfterDelete
async onAfterGetList(
context: IApiSubscriberRouteExecutionContext<PostEntity, PostEntity[]>
): Promise<PostEntity[] | undefined> {
const posts = context.result;
// Track analytics
await this.analyticsService.trackView("post_list", {
count: posts.length,
user: context.DATA.authenticationRequest?.user?.id,
});
return posts;
}
Error Hooks
Called when an operation fails:
onBeforeErrorCreate
, onAfterErrorCreate
async onBeforeErrorCreate(
context: IApiSubscriberRouteErrorExecutionContext<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>,
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 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 onBeforeErrorGetMany(
context: IApiSubscriberRouteErrorExecutionContext<PostEntity>,
error: Error
): Promise<void> { /* ... */ }
async onAfterErrorGetMany(
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 access to HTTP context:
async onBeforeCreate(context: IApiSubscriberRouteExecutionContext): Promise<any> {
// Authenticated user
const user = context.DATA.authenticationRequest?.user;
// Request headers
const contentType = context.DATA.headers["content-type"];
const userAgent = context.DATA.headers["user-agent"];
// Client IP
const clientIp = context.DATA.ip;
// Method metadata
const method = context.DATA.method; // "create"
const entityMetadata = context.DATA.entityMetadata;
return context.result;
}
Practical Examples
IP-Based Access Control
ip-restriction.subscriber.ts
@Injectable()
@ApiRouteSubscriber({ entity: SensitiveEntity, priority: 100 })
export class IpRestrictionSubscriber extends ApiRouteSubscriberBase<SensitiveEntity> {
private readonly allowedIps = ["192.168.1.1", "10.0.0.1"];
async onBeforeCreate(context: IApiSubscriberRouteExecutionContext): Promise<any> {
const clientIp = context.DATA.ip;
if (!this.allowedIps.includes(clientIp)) {
throw new ForbiddenException(`Access denied from 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: IApiSubscriberRouteExecutionContext): Promise<any> {
const userId = context.DATA.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: IApiSubscriberRouteExecutionContext): Promise<any> {
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: IApiSubscriberRouteExecutionContext<PostEntity, 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: IApiSubscriberRouteExecutionContext<PostEntity, 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
- Function Subscribers - Service-level hooks
- Execution Context - Context interfaces reference
- Lifecycle - Detailed execution flow
Last updated on