Claude Code for NestJS: Modules, Guards, Interceptors, and Dependency Injection — Claude Skills 360 Blog
Blog / Development / Claude Code for NestJS: Modules, Guards, Interceptors, and Dependency Injection
Development

Claude Code for NestJS: Modules, Guards, Interceptors, and Dependency Injection

Published: August 11, 2026
Read time: 10 min read
By: Claude Skills 360

NestJS brings Angular-style architecture to Node.js backends — modules, decorators, and a powerful dependency injection container that makes large APIs maintainable. Claude Code generates NestJS code correctly: understanding the decorator system, the module boundary rules, and the difference between guards, interceptors, and middleware.

This guide covers NestJS with Claude Code: module architecture, guards for authentication and authorization, interceptors for cross-cutting concerns, custom decorators, and testing.

Module Architecture with CLAUDE.md

## NestJS Architecture
- NestJS 10 with TypeScript strict mode
- Each domain has its own module: UsersModule, OrdersModule, AuthModule
- Shared utilities in SharedModule (exported from AppModule)
- Repository pattern: Entities in entities/, repos in repositories/
- DTOs in dto/ with class-validator decorators — never accept raw request body
- Guards: JwtAuthGuard (global), RolesGuard (per endpoint)
- Database: TypeORM with PostgreSQL

## Module convention
- Module imports: only import what the module needs
- Providers exported: only export what other modules consume
- No circular module dependencies — use forwardRef() only as last resort

Core Module Structure

Generate the full module structure for an Orders domain.
Needs: create, read, list (paginated), update status, and cancel.
// src/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { OrdersRepository } from './orders.repository';
import { Order } from './entities/order.entity';
import { OrderItem } from './entities/order-item.entity';
import { ProductsModule } from '../products/products.module'; // For stock checks

@Module({
  imports: [
    TypeOrmModule.forFeature([Order, OrderItem]),
    ProductsModule, // Import to use ProductsService
  ],
  controllers: [OrdersController],
  providers: [OrdersService, OrdersRepository],
  exports: [OrdersService], // Export for AdminModule to query orders
})
export class OrdersModule {}
// src/orders/dto/create-order.dto.ts
import { IsArray, IsUUID, IsInt, Min, Max, ArrayMinSize, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class OrderItemDto {
  @IsUUID()
  productId: string;

  @IsInt()
  @Min(1)
  @Max(100)
  quantity: number;
}

export class CreateOrderDto {
  @IsArray()
  @ArrayMinSize(1)
  @ValidateNested({ each: true })
  @Type(() => OrderItemDto)
  items: OrderItemDto[];

  @IsUUID()
  shippingAddressId: string;
}
// src/orders/orders.controller.ts
import {
  Controller, Get, Post, Patch, Body, Param, Query,
  ParseUUIDPipe, UseGuards, HttpCode, HttpStatus,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';
import { PaginationDto } from '../common/dto/pagination.dto';
import { User } from '../users/entities/user.entity';

@Controller('orders')
@UseGuards(JwtAuthGuard) // All endpoints require auth
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() dto: CreateOrderDto, @CurrentUser() user: User) {
    return this.ordersService.create(dto, user.id);
  }

  @Get()
  findAll(@Query() pagination: PaginationDto, @CurrentUser() user: User) {
    return this.ordersService.findByUser(user.id, pagination);
  }

  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: User) {
    return this.ordersService.findOneForUser(id, user.id);
  }

  @Patch(':id/cancel')
  cancel(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: User) {
    return this.ordersService.cancel(id, user.id);
  }

  // Admin-only endpoint
  @Patch(':id/status')
  @UseGuards(RolesGuard)
  @Roles('admin')
  updateStatus(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('status') status: string,
  ) {
    return this.ordersService.updateStatus(id, status);
  }
}

Guards: Authentication and Authorization

Create a JWT guard and a roles-based authorization guard.
The JWT guard should be applied globally. Roles guard only where needed.
// src/auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    // Allow routes marked with @Public()
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    return super.canActivate(context);
  }

  handleRequest(err: any, user: any, info: any) {
    if (err || !user) {
      throw err || new UnauthorizedException(info?.message ?? 'Authentication required');
    }
    return user;
  }
}
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // No @Roles() decorator — any authenticated user can proceed
    if (!requiredRoles?.length) return true;

    const { user } = context.switchToHttp().getRequest();

    const hasRole = requiredRoles.some(role => user?.roles?.includes(role));
    if (!hasRole) {
      throw new ForbiddenException(
        `Requires one of: ${requiredRoles.join(', ')}`,
      );
    }

    return true;
  }
}
// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

// src/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// src/auth/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
);

Register the JWT guard globally so every route is protected by default:

// src/app.module.ts
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    { provide: APP_GUARD, useClass: JwtAuthGuard }, // Global — all routes protected
  ],
})
export class AppModule {}

Interceptors: Logging, Caching, and Transform

Add an interceptor that:
1. Logs request duration for every endpoint
2. Transforms all responses to wrap in { data, meta } envelope
// src/common/interceptors/logging.interceptor.ts
import {
  Injectable, NestInterceptor, ExecutionContext,
  CallHandler, Logger,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('HTTP');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const { method, url, user } = req;
    const start = Date.now();

    return next.handle().pipe(
      tap({
        next: () => {
          const duration = Date.now() - start;
          this.logger.log(`${method} ${url} ${duration}ms userId=${user?.id ?? 'anonymous'}`);
        },
        error: (err) => {
          const duration = Date.now() - start;
          this.logger.error(`${method} ${url} ${duration}ms ERROR: ${err.message}`);
        },
      }),
    );
  }
}
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, map } from 'rxjs';

export interface Response<T> {
  data: T;
  meta: { timestamp: string; path: string };
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    const req = context.switchToHttp().getRequest();

    return next.handle().pipe(
      map(data => ({
        data,
        meta: {
          timestamp: new Date().toISOString(),
          path: req.url,
        },
      })),
    );
  }
}
// src/common/interceptors/cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of, tap } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { CACHE_TTL_KEY } from '../decorators/cacheable.decorator';

@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
  private readonly cache = new Map<string, { data: any; expiresAt: number }>();

  constructor(private reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();

    // Only cache GET requests
    if (req.method !== 'GET') return next.handle();

    const ttl = this.reflector.get<number>(CACHE_TTL_KEY, context.getHandler());
    if (!ttl) return next.handle();

    const cacheKey = `${req.url}:${req.user?.id ?? 'anon'}`;
    const cached = this.cache.get(cacheKey);

    if (cached && cached.expiresAt > Date.now()) {
      return of(cached.data);
    }

    return next.handle().pipe(
      tap(data => {
        this.cache.set(cacheKey, { data, expiresAt: Date.now() + ttl * 1000 });
      }),
    );
  }
}

// Usage: @Cacheable(60) — cache for 60 seconds
export const CACHE_TTL_KEY = 'cache:ttl';
export const Cacheable = (ttl: number) => SetMetadata(CACHE_TTL_KEY, ttl);

Custom Pipes and Exception Filters

// src/common/filters/http-exception.filter.ts
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal server error';

    if (status >= 500) {
      this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : String(exception));
    }

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof message === 'object' ? message : { error: message },
    });
  }
}

Testing NestJS

Write unit tests for OrdersService using Jest.
Mock the repository — don't hit the database.
// src/orders/orders.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException, ForbiddenException } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { OrdersRepository } from './orders.repository';
import { ProductsService } from '../products/products.service';

const mockOrder = {
  id: 'order-1',
  userId: 'user-1',
  status: 'PENDING',
  items: [{ productId: 'prod-1', quantity: 2, priceCents: 1000 }],
};

describe('OrdersService', () => {
  let service: OrdersService;
  let repo: jest.Mocked<OrdersRepository>;
  let productsService: jest.Mocked<ProductsService>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OrdersService,
        {
          provide: OrdersRepository,
          useValue: {
            create: jest.fn(),
            findOneByIdAndUser: jest.fn(),
            findByUser: jest.fn(),
            save: jest.fn(),
          },
        },
        {
          provide: ProductsService,
          useValue: {
            findManyByIds: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get(OrdersService);
    repo = module.get(OrdersRepository);
    productsService = module.get(ProductsService);
  });

  describe('cancel', () => {
    it('cancels a pending order belonging to the user', async () => {
      repo.findOneByIdAndUser.mockResolvedValue({ ...mockOrder });
      repo.save.mockResolvedValue({ ...mockOrder, status: 'CANCELLED' });

      const result = await service.cancel('order-1', 'user-1');

      expect(result.status).toBe('CANCELLED');
      expect(repo.save).toHaveBeenCalledWith(
        expect.objectContaining({ status: 'CANCELLED' }),
      );
    });

    it('throws NotFoundException when order not found or not owned', async () => {
      repo.findOneByIdAndUser.mockResolvedValue(null);

      await expect(service.cancel('order-1', 'user-2')).rejects.toThrow(NotFoundException);
    });

    it('throws ForbiddenException when order is already shipped', async () => {
      repo.findOneByIdAndUser.mockResolvedValue({ ...mockOrder, status: 'SHIPPED' });

      await expect(service.cancel('order-1', 'user-1')).rejects.toThrow(ForbiddenException);
    });
  });
});

For building complete API backends with TypeORM and PostgreSQL, see the database guide. For securing NestJS APIs with OAuth and JWT refresh token rotation, see the auth guide. The Claude Skills 360 bundle includes NestJS skill sets covering the full module lifecycle, microservice transport adapters, and OpenAPI generation. Start with the free tier to try NestJS code generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free