Mario Brusarosco

nestjs creating a new module

In the ground since Sat Nov 08 2025

Last watered inSat Nov 08 2025

Related Topics

Referece Links

Recap

A NestJS module encapsulates related functionality (controller + service + dependencies) into a reusable, isolated unit. Creating a new module involves 4 files: the module definition, controller, service, and registration in the app module.

Real Example: Created sdk-runtime module for Peek-a-boo to separate runtime API endpoints from admin endpoints, demonstrating proper separation of concerns in NestJS.

Why Create a Module?

Modules help organize code by domain/feature:

  • Separation of Concerns - Runtime API vs Admin API
  • Dependency Management - Each module declares its own dependencies
  • Reusability - Modules can be imported by other modules
  • Testability - Isolated modules are easier to test

When to Create a New Module?

Create a new module when:

  1. Different Domain - User management vs feature flags vs payments
  2. Different Auth Requirements - Public API vs admin API
  3. Different Responsibility - CRUD operations vs analytics vs webhooks
  4. Different Client - Dashboard vs mobile app vs SDK

Example from Peek-a-boo:

  • feature-flags module → Admin CRUD operations (dashboard)
  • sdk-runtime module → Runtime flag fetching (client SDKs)

Same domain (feature flags), but different purposes warrant separate modules.

Anatomy of a NestJS Module

Directory Structure

1src/domains/
2├── feature-flags/ # Existing admin module
3│ ├── feature-flags.controller.ts
4│ ├── feature-flags.service.ts
5│ └── feature-flags.module.ts
6└── sdk-runtime/ # New runtime module
7 ├── sdk-runtime.controller.ts
8 ├── sdk-runtime.service.ts
9 └── sdk-runtime.module.ts

The Four Essential Files

1. Service (Business Logic)

1// sdk-runtime.service.ts
2import { Injectable } from '@nestjs/common';
3import { PrismaService } from '@/prisma/prisma.service';
4import type { FeatureFlag, Environment } from '@peek-a-boo/core';
5
6@Injectable()
7export class SdkRuntimeService {
8 constructor(private prismaService: PrismaService) {}
9
10 async getFlagsByEnvironment(
11 projectId: string,
12 environment: Environment,
13 ): Promise<FeatureFlag[]> {
14 return this.prismaService.featureFlag.findMany({
15 where: {
16 projectId,
17 environment,
18 },
19 });
20 }
21
22 async getFlagByKey(
23 projectId: string,
24 key: string,
25 environment: Environment,
26 ): Promise<FeatureFlag | null> {
27 return this.prismaService.featureFlag.findFirst({
28 where: {
29 projectId,
30 key,
31 environment,
32 },
33 });
34 }
35}

Key Points:

  • @Injectable() decorator makes it available for dependency injection
  • Constructor injection for dependencies (PrismaService)
  • Pure business logic - no HTTP concerns
  • Typed return values for type safety

2. Controller (HTTP Layer)

1// sdk-runtime.controller.ts
2import { Controller, Get, Query, Param, BadRequestException } from '@nestjs/common';
3import { SdkRuntimeService } from './sdk-runtime.service';
4import type { FeatureFlag, Environment } from '@peek-a-boo/core';
5
6@Controller('api/v1')
7export class SdkRuntimeController {
8 constructor(private readonly sdkRuntimeService: SdkRuntimeService) {}
9
10 @Get('flags')
11 async getFlags(
12 @Query('projectId') projectId: string,
13 @Query('environment') environment: string,
14 ): Promise<{ flags: FeatureFlag[]; environment: string }> {
15 // Validation
16 if (!projectId) {
17 throw new BadRequestException('projectId query parameter is required');
18 }
19
20 if (!environment) {
21 throw new BadRequestException('environment query parameter is required');
22 }
23
24 // Validate environment enum
25 const validEnvironments = ['DEVELOPMENT', 'STAGING', 'PRODUCTION'];
26 const env = environment.toUpperCase();
27 if (!validEnvironments.includes(env)) {
28 throw new BadRequestException(
29 `Invalid environment. Must be one of: ${validEnvironments.join(', ')}`,
30 );
31 }
32
33 const flags = await this.sdkRuntimeService.getFlagsByEnvironment(
34 projectId,
35 env as Environment,
36 );
37
38 return {
39 flags,
40 environment: env,
41 };
42 }
43
44 @Get('flags/:key')
45 async getFlag(
46 @Param('key') key: string,
47 @Query('projectId') projectId: string,
48 @Query('environment') environment: string,
49 ): Promise<FeatureFlag> {
50 if (!projectId) {
51 throw new BadRequestException('projectId query parameter is required');
52 }
53
54 if (!environment) {
55 throw new BadRequestException('environment query parameter is required');
56 }
57
58 const validEnvironments = ['DEVELOPMENT', 'STAGING', 'PRODUCTION'];
59 const env = environment.toUpperCase();
60 if (!validEnvironments.includes(env)) {
61 throw new BadRequestException(
62 `Invalid environment. Must be one of: ${validEnvironments.join(', ')}`,
63 );
64 }
65
66 const flag = await this.sdkRuntimeService.getFlagByKey(
67 projectId,
68 key,
69 env as Environment,
70 );
71
72 if (!flag) {
73 throw new BadRequestException(
74 `Flag with key "${key}" not found in ${env} environment`,
75 );
76 }
77
78 return flag;
79 }
80}

Key Points:

  • @Controller('api/v1') sets the base route
  • @Get('flags') creates route: GET /api/v1/flags
  • @Query() extracts query parameters
  • @Param() extracts route parameters
  • Validation happens here, not in service
  • Service is injected via constructor

3. Module Definition

1// sdk-runtime.module.ts
2import { Module } from '@nestjs/common';
3import { SdkRuntimeController } from './sdk-runtime.controller';
4import { SdkRuntimeService } from './sdk-runtime.service';
5import { PrismaModule } from '@/prisma/prisma.module';
6
7@Module({
8 imports: [PrismaModule], // Modules this module depends on
9 controllers: [SdkRuntimeController], // HTTP controllers
10 providers: [SdkRuntimeService], // Services/providers
11 exports: [SdkRuntimeService], // What other modules can import
12})
13export class SdkRuntimeModule {}

Key Points:

  • imports - Modules needed by this module
  • controllers - HTTP endpoints
  • providers - Services, repositories, etc.
  • exports - Make service available to other modules (optional)

4. App Module Registration

1// app.module.ts
2import { Module } from '@nestjs/common';
3import { ConfigModule } from '@nestjs/config';
4import { PrismaModule } from './prisma/prisma.module';
5import { FeatureFlagsModule } from '@/domains/feature-flags/feature-flags.module';
6import { SdkRuntimeModule } from '@/domains/sdk-runtime/sdk-runtime.module';
7
8@Module({
9 imports: [
10 ConfigModule.forRoot({
11 isGlobal: true,
12 }),
13 PrismaModule,
14 FeatureFlagsModule,
15 SdkRuntimeModule, // ← Register new module here
16 ],
17})
18export class AppModule {}

Step-by-Step Creation Process

Step 1: Create Directory

1mkdir -p apps/sdk-service/src/domains/sdk-runtime

Step 2: Create Service

Create sdk-runtime.service.ts with:

  • @Injectable() decorator
  • Constructor dependencies
  • Business logic methods
  • Typed return values

Step 3: Create Controller

Create sdk-runtime.controller.ts with:

  • @Controller('route') decorator
  • Route handlers with @Get(), @Post(), etc.
  • Service injection via constructor
  • Input validation
  • HTTP-specific error handling

Step 4: Create Module

Create sdk-runtime.module.ts with:

  • @Module() decorator
  • Import dependencies
  • Declare controllers and providers
  • Export what's needed

Step 5: Register in App Module

Add new module to app.module.ts imports array.

Step 6: Test

1# Development server should auto-reload
2# Test endpoints:
3curl http://localhost:6001/api/v1/flags?projectId=xxx&environment=development

Common Patterns

Pattern 1: Shared Dependencies

If multiple modules need the same service:

1// shared.module.ts
2@Module({
3 providers: [SharedService],
4 exports: [SharedService], // Make available to importers
5})
6export class SharedModule {}
7
8// feature.module.ts
9@Module({
10 imports: [SharedModule], // Import to use SharedService
11 providers: [FeatureService],
12})
13export class FeatureModule {}

Pattern 2: Global Modules

For truly global services (config, logging):

1@Global() // ← Makes module available everywhere
2@Module({
3 providers: [ConfigService],
4 exports: [ConfigService],
5})
6export class ConfigModule {}

Pattern 3: Dynamic Modules

For modules that need configuration:

1@Module({})
2export class DatabaseModule {
3 static forRoot(options: DatabaseOptions): DynamicModule {
4 return {
5 module: DatabaseModule,
6 providers: [
7 {
8 provide: 'DATABASE_OPTIONS',
9 useValue: options,
10 },
11 DatabaseService,
12 ],
13 exports: [DatabaseService],
14 };
15 }
16}
17
18// Usage
19@Module({
20 imports: [
21 DatabaseModule.forRoot({ host: 'localhost', port: 5432 })
22 ],
23})
24export class AppModule {}

Dependency Injection Explained

How It Works

1// 1. Service declares it's injectable
2@Injectable()
3export class MyService {
4 someMethod() { }
5}
6
7// 2. Module registers it as provider
8@Module({
9 providers: [MyService],
10})
11export class MyModule {}
12
13// 3. Other classes can inject it
14@Injectable()
15export class OtherService {
16 constructor(private myService: MyService) {}
17 // ↑ NestJS automatically provides instance
18}

The Magic

NestJS:

  1. Scans all modules on startup
  2. Creates a dependency graph
  3. Instantiates services in correct order
  4. Injects dependencies via constructor
  5. Manages lifecycle (singleton by default)

Best Practices

1. One Responsibility Per Module

❌ Bad: CommonModule with unrelated stuff ✅ Good: AuthModule, UserModule, EmailModule

2. Controller Does HTTP, Service Does Logic

❌ Bad:

1@Get()
2async getUsers() {
3 // Database queries in controller
4 return this.prismaService.user.findMany();
5}

✅ Good:

1@Get()
2async getUsers() {
3 return this.userService.findAll(); // Delegate to service
4}

3. Validate in Controller, Not Service

❌ Bad:

1// In service
2async getFlag(key: string) {
3 if (!key) throw new Error('Key required'); // HTTP concern in service
4}

✅ Good:

1// In controller
2@Get(':key')
3async getFlag(@Param('key') key: string) {
4 if (!key) throw new BadRequestException('Key required');
5 return this.service.getFlag(key);
6}

4. Type Everything

1// Service method
2async getFlagsByEnvironment(
3 projectId: string,
4 environment: Environment,
5): Promise<FeatureFlag[]> { // ← Typed return
6 // ...
7}

5. Export Only What's Needed

1@Module({
2 providers: [ServiceA, ServiceB],
3 exports: [ServiceA], // Only ServiceA is public
4})

Testing Your Module

Unit Test the Service

1import { Test, TestingModule } from '@nestjs/testing';
2import { SdkRuntimeService } from './sdk-runtime.service';
3import { PrismaService } from '@/prisma/prisma.service';
4
5describe('SdkRuntimeService', () => {
6 let service: SdkRuntimeService;
7 let prisma: PrismaService;
8
9 beforeEach(async () => {
10 const module: TestingModule = await Test.createTestingModule({
11 providers: [
12 SdkRuntimeService,
13 {
14 provide: PrismaService,
15 useValue: {
16 featureFlag: {
17 findMany: jest.fn(),
18 findFirst: jest.fn(),
19 },
20 },
21 },
22 ],
23 }).compile();
24
25 service = module.get<SdkRuntimeService>(SdkRuntimeService);
26 prisma = module.get<PrismaService>(PrismaService);
27 });
28
29 it('should fetch flags by environment', async () => {
30 const mockFlags = [{ key: 'test', enabled: true }];
31 jest.spyOn(prisma.featureFlag, 'findMany').mockResolvedValue(mockFlags);
32
33 const result = await service.getFlagsByEnvironment('project-1', 'DEVELOPMENT');
34 expect(result).toEqual(mockFlags);
35 });
36});

Integration Test the Controller

1import { Test, TestingModule } from '@nestjs/testing';
2import { SdkRuntimeController } from './sdk-runtime.controller';
3import { SdkRuntimeService } from './sdk-runtime.service';
4
5describe('SdkRuntimeController', () => {
6 let controller: SdkRuntimeController;
7 let service: SdkRuntimeService;
8
9 beforeEach(async () => {
10 const module: TestingModule = await Test.createTestingModule({
11 controllers: [SdkRuntimeController],
12 providers: [
13 {
14 provide: SdkRuntimeService,
15 useValue: {
16 getFlagsByEnvironment: jest.fn(),
17 },
18 },
19 ],
20 }).compile();
21
22 controller = module.get<SdkRuntimeController>(SdkRuntimeController);
23 service = module.get<SdkRuntimeService>(SdkRuntimeService);
24 });
25
26 it('should return flags', async () => {
27 const mockFlags = [{ key: 'test', enabled: true }];
28 jest.spyOn(service, 'getFlagsByEnvironment').mockResolvedValue(mockFlags);
29
30 const result = await controller.getFlags('project-1', 'development');
31 expect(result.flags).toEqual(mockFlags);
32 expect(result.environment).toBe('DEVELOPMENT');
33 });
34});

Troubleshooting

"Cannot resolve dependency"

Issue: NestJS can't inject a dependency.

Solution:

  1. Ensure service has @Injectable() decorator
  2. Ensure service is in module's providers array
  3. If importing from another module, ensure it's exported there and imported here

"Circular dependency detected"

Issue: Module A imports Module B which imports Module A.

Solution:

  1. Use forwardRef():
1@Module({
2 imports: [forwardRef(() => OtherModule)],
3})
  1. Or refactor to eliminate circular dependency

Routes Not Working

Issue: Endpoints return 404.

Solution:

  1. Ensure module is imported in AppModule
  2. Check @Controller() route matches your URL
  3. Verify method decorators (@Get(), @Post(), etc.)

Commands Reference

1# Create module directory
2mkdir -p src/domains/module-name
3
4# Generate module with NestJS CLI (optional)
5nest generate module domains/module-name
6nest generate controller domains/module-name
7nest generate service domains/module-name
8
9# Test endpoints
10curl http://localhost:6001/your-route

Real-World Example: SDK Runtime Module

The sdk-runtime module demonstrates:

  • ✅ Separation from admin module
  • ✅ Different route prefix (/api/v1 vs /feature-flags)
  • ✅ Proper dependency injection (PrismaService)
  • ✅ Input validation in controller
  • ✅ Business logic in service
  • ✅ Type safety throughout
  • ✅ Clean, testable architecture

Files created:

  • sdk-runtime.service.ts - 45 lines
  • sdk-runtime.controller.ts - 75 lines
  • sdk-runtime.module.ts - 12 lines
  • Registration in app.module.ts - 1 line

Total time to create: ~15 minutes for a production-ready module.

Key Takeaways

  1. Modules = Organization - Group related functionality
  2. Controllers = HTTP - Handle requests/responses
  3. Services = Logic - Business rules and data access
  4. DI = Magic - NestJS handles instantiation
  5. Testing = Isolation - Mock dependencies easily
  6. Separation = Maintainability - Runtime vs Admin APIs

A well-structured module is self-contained, testable, and easy to understand.