Lifecycle
Understanding the execution order of subscribers and operations is crucial for implementing complex business logic correctly.
Complete Execution Flow
Create Operation
1. HTTP Request arrives at controller
2. Route subscribers: onBeforeCreate (priority: high → low)
3. Controller transformers (request)
4. Controller validators
5. Service method: create() called
6. Function subscribers: onBeforeCreate (priority: high → low)
7. Repository: save() executed
8. Function subscribers: onAfterCreate (priority: low → high)
9. Service returns result
10. Controller transformers (response)
11. Route subscribers: onAfterCreate (priority: low → high)
12. HTTP Response sent to client
Update Operation
1. HTTP Request arrives at controller
2. Route subscribers: onBeforeUpdate (priority: high → low)
3. Controller transformers (request)
4. Controller validators
5. Service method: update() called
6. Function subscribers: onBeforeUpdate (priority: high → low)
7. Repository: findOne() to get existing entity
8. Repository: save() executed with updates
9. Function subscribers: onAfterUpdate (priority: low → high)
10. Service returns result
11. Controller transformers (response)
12. Route subscribers: onAfterUpdate (priority: low → high)
13. HTTP Response sent to client
Get Operation
1. HTTP Request arrives at controller
2. Route subscribers: onBeforeGet (priority: high → low)
3. Service method: get() called
4. Function subscribers: onBeforeGet (priority: high → low)
5. Repository: findOne() executed
6. Function subscribers: onAfterGet (priority: low → high)
7. Service returns result
8. Controller transformers (response)
9. Route subscribers: onAfterGet (priority: low → high)
10. HTTP Response sent to client
Get List Operation
1. HTTP Request arrives at controller
2. Route subscribers: onBeforeGetList (priority: high → low)
3. Query parameters parsed and validated
4. Service method: getList() called
5. Function subscribers: onBeforeGetList (priority: high → low)
6. Repository: find() and count() executed
7. Function subscribers: onAfterGetList (priority: low → high)
8. Service returns result with pagination metadata
9. Controller transformers (response)
10. Route subscribers: onAfterGetList (priority: low → high)
11. HTTP Response sent to client
Delete Operation
1. HTTP Request arrives at controller
2. Route subscribers: onBeforeDelete (priority: high → low)
3. Service method: delete() called
4. Function subscribers: onBeforeDelete (priority: high → low)
5. Repository: findOne() to verify existence
6. Repository: delete() or softDelete() executed
7. Function subscribers: onAfterDelete (priority: low → high)
8. Service returns result
9. Route subscribers: onAfterDelete (priority: low → high)
10. HTTP Response sent to client
Error Flow
When an error occurs at any step:
1. Error thrown at step N
2. Execution stops
3. onBeforeError hooks called (priority: high → low)
- Route level (if error in controller/route subscriber)
- Function level (if error in service/function subscriber)
4. onAfterError hooks called (priority: high → low)
- Function level
- Route level
5. Error propagated to exception filter
6. Error response sent to client
Priority Execution
Before Hooks
Execute in priority order (high to low):
// Priority 100 executes first
@ApiRouteSubscriber({ entity: Post, priority: 100 })
export class SecuritySubscriber {
async onBeforeCreate(context) {
console.log("1. Security check");
return context.result;
}
}
// Priority 50 executes second
@ApiRouteSubscriber({ entity: Post, priority: 50 })
export class ValidationSubscriber {
async onBeforeCreate(context) {
console.log("2. Validation");
return context.result;
}
}
// Priority 10 executes last
@ApiRouteSubscriber({ entity: Post, priority: 10 })
export class LoggingSubscriber {
async onBeforeCreate(context) {
console.log("3. Logging");
return context.result;
}
}
After Hooks
Execute in reverse priority order (low to high):
// Priority 10 executes first
@ApiRouteSubscriber({ entity: Post, priority: 10 })
export class LoggingSubscriber {
async onAfterCreate(context) {
console.log("1. Logging result");
return context.result;
}
}
// Priority 50 executes second
@ApiRouteSubscriber({ entity: Post, priority: 50 })
export class CacheSubscriber {
async onAfterCreate(context) {
console.log("2. Update cache");
return context.result;
}
}
// Priority 100 executes last
@ApiRouteSubscriber({ entity: Post, priority: 100 })
export class NotificationSubscriber {
async onAfterCreate(context) {
console.log("3. Send notifications");
return context.result;
}
}
Detailed Example with Logging
example-with-logging.ts
// Route Subscriber
@Injectable()
@ApiRouteSubscriber({ entity: PostEntity, priority: 50 })
export class PostRouteSubscriber extends ApiRouteSubscriberBase<PostEntity> {
async onBeforeCreate(context: any): Promise<any> {
console.log("[Route] onBeforeCreate - START");
return context.result;
}
async onAfterCreate(context: any): Promise<PostEntity> {
console.log("[Route] onAfterCreate - END");
return context.result;
}
}
// Function Subscriber
@Injectable()
@ApiFunctionSubscriber({ entity: PostEntity, priority: 50 })
export class PostFunctionSubscriber extends ApiFunctionSubscriberBase<PostEntity> {
async onBeforeCreate(context: any): Promise<any> {
console.log("[Function] onBeforeCreate - START");
return context.result;
}
async onAfterCreate(context: any): Promise<PostEntity> {
console.log("[Function] onAfterCreate - END");
return context.result;
}
}
// Service
@Injectable()
@ApiService({ entity: PostEntity })
@ApiServiceObservable()
export class PostService extends ApiServiceBase<PostEntity> {
async create(properties: any): Promise<PostEntity> {
console.log("[Service] create - Repository operation");
return super.create(properties);
}
}
// Console output for POST /posts:
// [Route] onBeforeCreate - START
// [Function] onBeforeCreate - START
// [Service] create - Repository operation
// [Function] onAfterCreate - END
// [Route] onAfterCreate - END
Async Operations
All subscriber hooks are async and execute sequentially:
async onBeforeCreate(context: any): Promise<any> {
// Operation 1 completes first
await this.validateData(context.result.body);
// Operation 2 completes second
await this.checkPermissions(context.DATA.authenticationRequest?.user);
// Operation 3 completes third
await this.enrichData(context.result.body);
return context.result;
}
Short-Circuiting
Throw an error to stop execution:
async onBeforeCreate(context: any): Promise<any> {
const user = context.DATA.authenticationRequest?.user;
if (!user || user.role !== "admin") {
// Stops here, error hooks are called
throw new ForbiddenException("Admin access required");
}
return context.result;
}
Modifying Data Through Pipeline
Each subscriber can modify the result:
// Subscriber 1 (Priority 100)
async onBeforeCreate(context: any): Promise<any> {
context.result.body.field1 = "value1";
return context.result; // Passes to next subscriber
}
// Subscriber 2 (Priority 50)
async onBeforeCreate(context: any): Promise<any> {
// Receives modified context from Subscriber 1
context.result.body.field2 = "value2";
return context.result; // Passes to next subscriber
}
// Subscriber 3 (Priority 10)
async onBeforeCreate(context: any): Promise<any> {
// Receives context with field1 and field2
context.result.body.field3 = "value3";
return context.result; // Final result
}
Transaction Boundaries
Function subscribers operate within database transactions:
async onBeforeCreate(
context: IApiSubscriberFunctionExecutionContext<PostEntity, any>
): Promise<any> {
const manager = context.DATA.transactionManager;
if (manager) {
// All operations within same transaction
const tags = await manager.save(TagEntity, [...]);
const category = await manager.save(CategoryEntity, {...});
context.result.body.tags = tags;
context.result.body.categoryId = category.id;
}
return context.result;
// If any operation fails, entire transaction rolls back
}
Best Practices
1. Use Priority Wisely
// Validation should happen first
@ApiRouteSubscriber({ entity: Post, priority: 100 })
export class ValidationSubscriber {}
// Business logic in the middle
@ApiRouteSubscriber({ entity: Post, priority: 50 })
export class BusinessLogicSubscriber {}
// Logging and notifications last
@ApiRouteSubscriber({ entity: Post, priority: 10 })
export class NotificationSubscriber {}
2. Keep Hooks Focused
// Good: One responsibility per subscriber
@ApiFunctionSubscriber({ entity: Post })
export class SlugGeneratorSubscriber {
async onBeforeCreate(context: any) {
context.result.body.slug = this.generateSlug(context.result.body.title);
return context.result;
}
}
// Bad: Multiple responsibilities
@ApiFunctionSubscriber({ entity: Post })
export class MixedSubscriber {
async onBeforeCreate(context: any) {
// Don't do this
context.result.body.slug = this.generateSlug(context.result.body.title);
await this.sendNotification();
await this.updateCache();
return context.result;
}
}
3. Handle Errors Gracefully
async onAfterErrorCreate(
context: IApiSubscriberRouteErrorExecutionContext<PostEntity>,
error: Error
): Promise<void> {
try {
// Cleanup operations
await this.cleanup(context.ENTITY);
} catch (cleanupError) {
// Don't throw errors in error hooks
this.logger.error("Cleanup failed", cleanupError);
}
}
Next Steps
- Route Subscribers - Implementing route hooks
- Function Subscribers - Implementing function hooks
- Execution Context - Understanding contexts
Last updated on