Request Lifecycle
What runs before and after your handler, and where to put cross-cutting logic.
When a client calls a tool, your method is not the first or last thing to run. The call passes through a fixed pipeline, and each stage has one job. Knowing the order tells you where logic belongs.
The stages
Guards decide whether the call may run. A guard returns true or false. If any guard says no, nothing else runs, including your handler. Use guards for authentication and permissions. See Guards.
Middleware wraps the whole call. It runs code before and after everything that follows, which makes it the place for logging, request IDs, and timing. See Middleware.
Interceptors wrap the handler and can change its result. Use them to add metadata, mask sensitive fields, or cache responses. See Interceptors.
Pipes transform and validate input. Your Zod schema already validates input; pipes handle extra work such as trimming strings or coercing types. See Pipes.
Exception filters handle errors. They catch anything thrown during the call and turn it into a consistent error response. See Error Handling.
Attaching stages to a tool
Each stage is a class you attach with a decorator. They stack, and you only add the ones you need:
import { ToolDecorator as Tool, UseGuards, UseMiddleware, ExecutionContext, z } from '@nitrostack/core';
import { AdminGuard } from './admin.guard.js';
import { LoggingMiddleware } from './logging.middleware.js';
export class AdminTools {
@Tool({
name: 'delete_product',
description: 'Remove a product from the catalog',
inputSchema: z.object({ id: z.string() }),
})
@UseGuards(AdminGuard)
@UseMiddleware(LoggingMiddleware)
async deleteProduct(input: { id: string }, ctx: ExecutionContext) {
ctx.logger.info('Deleting product', { id: input.id, by: ctx.auth?.subject });
return { deleted: input.id };
}
}A guard is a class with a canActivate method:
import { Guard, ExecutionContext, Injectable } from '@nitrostack/core';
@Injectable()
export class AdminGuard implements Guard {
async canActivate(ctx: ExecutionContext): Promise<boolean> {
return ctx.auth?.scopes?.includes('admin') ?? false;
}
}The execution context
Every stage and every handler receives the same ExecutionContext for the call. It carries a logger, a requestId for tracing, the toolName, and auth, which guards fill in once a caller is identified. Reading ctx.auth in your handler is how you know who is calling. The full shape is in Server Concepts.
