Core Concepts

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.

Before your handler
Guards
Middleware
Interceptors
Pipes
Handler
After your handler
Interceptors
Exception filters
Response
If a guard returns false, the call stops there and nothing after it runs.

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:

admin.guard.ts
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.

Go deeper

On this page