ication lifecycle but stateless regarding HTTP concerns. The @Injectable() decorator registers this class with the NestJS DI container, allowing it to be injected into controllers or other services.
File: src/inventory/inventory.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterItemDto } from './dto/register-item.dto';
export interface StockRecord {
sku: string;
name: string;
count: number;
}
@Injectable()
export class InventoryService {
private ledger: StockRecord[] = [];
getLedger(): StockRecord[] {
return this.ledger;
}
register(payload: RegisterItemDto): StockRecord {
const record: StockRecord = {
sku: payload.sku,
name: payload.name,
count: payload.count,
};
this.ledger.push(record);
return record;
}
remove(sku: string): boolean {
const initialSize = this.ledger.length;
this.ledger = this.ledger.filter(item => item.sku !== sku);
return this.ledger.length < initialSize;
}
}
Rationale:
- Dependency Injection:
@Injectable() allows NestJS to instantiate InventoryService once (singleton by default) and share it across requests.
- Separation: The service knows nothing about HTTP methods, status codes, or request bodies. It only manages the
ledger array.
- Return Types: Methods return domain objects (
StockRecord) or booleans, keeping the service pure.
3. Map HTTP Routes (Controller)
The Controller acts as the entry point. It receives HTTP requests, extracts parameters, delegates to the Service, and returns responses. Decorators map methods to specific verbs and paths.
File: src/inventory/inventory.controller.ts
import { Controller, Get, Post, Delete, Body, Param, HttpCode, HttpStatus } from '@nestjs/common';
import { InventoryService } from './inventory.service';
import { RegisterItemDto } from './dto/register-item.dto';
@Controller('inventory')
export class InventoryController {
constructor(private readonly registry: InventoryService) {}
@Get()
listAll(): StockRecord[] {
return this.registry.getLedger();
}
@Post()
add(@Body() payload: RegisterItemDto): StockRecord {
return this.registry.register(payload);
}
@Delete(':sku')
@HttpCode(HttpStatus.OK)
purge(@Param('sku') sku: string): { result: string } {
const success = this.registry.remove(sku);
if (!success) {
return { result: 'not_found' };
}
return { result: 'removed' };
}
}
Rationale:
- Constructor Injection:
private readonly registry: InventoryService injects the service. NestJS resolves this dependency automatically based on the type.
- Decorators:
@Controller('inventory') sets the base path. @Get(), @Post(), and @Delete() map to HTTP verbs.
- Parameter Extraction:
@Body() deserializes the JSON payload into RegisterItemDto. @Param('sku') extracts the URL parameter.
- Status Codes:
@HttpCode(HttpStatus.OK) explicitly sets the response status, overriding default behaviors for clarity.
4. Wire the Module
Modules define the scope of providers and controllers. They tell NestJS which components belong together and manage the DI container's visibility.
File: src/inventory/inventory.module.ts
import { Module } from '@nestjs/common';
import { InventoryController } from './inventory.controller';
import { InventoryService } from './inventory.service';
@Module({
controllers: [InventoryController],
providers: [InventoryService],
})
export class InventoryModule {}
Rationale: Even for a small feature, creating a dedicated module establishes a scalable pattern. In larger applications, this module would be imported by AppModule. This structure supports lazy loading and feature isolation.
5. Bootstrap the Application
The entry point initializes the NestJS application factory and starts the HTTP server.
File: src/main.ts
import { NestFactory } from '@nestjs/core';
import { InventoryModule } from './inventory/inventory.module';
async function bootstrap() {
const app = await NestFactory.create(InventoryModule);
await app.listen(3000);
console.log('Inventory API active on port 3000');
}
bootstrap();
Rationale: NestFactory.create compiles the module graph and resolves all dependencies. The application listens on port 3000, ready to accept requests.
Pitfall Guide
1. Controller Logic Leakage
- Explanation: Developers often place business logic, data transformation, or validation directly in the Controller.
- Impact: Controllers become difficult to test and violate the Single Responsibility Principle. Logic cannot be reused by other services or CLI commands.
- Fix: Keep Controllers thin. They should only extract request data, call service methods, and format responses. Move all logic to the Service.
2. Missing @Injectable() Decorator
- Explanation: Forgetting to add
@Injectable() to a Service class prevents NestJS from managing it in the DI container.
- Impact: Runtime error:
Nest can't resolve dependencies. The framework cannot instantiate the service.
- Fix: Always decorate service classes with
@Injectable(). This signals to the compiler that the class participates in DI.
3. Module Registration Omission
- Explanation: Creating a Controller or Service but failing to list it in the
controllers or providers array of a @Module.
- Impact: The endpoint returns 404, or the service is undefined. NestJS only instantiates components declared in modules.
- Fix: Verify that every Controller and Service is registered in a module. Use feature modules to group related components.
4. Loose Type Definitions
- Explanation: Using
any types in DTOs or Service methods, or relying on implicit types.
- Impact: Loss of compile-time safety. Runtime errors occur when unexpected data shapes arrive. IDE autocomplete fails.
- Fix: Define strict interfaces for domain models and classes for DTOs. Enable
strict: true in tsconfig.json.
5. Parameter Parsing Errors
- Explanation: URL parameters arrive as strings. Passing them directly to methods expecting numbers or specific formats causes type mismatches.
- Impact: Logic failures, such as comparing a string ID to a number ID, resulting in missed records.
- Fix: Use
parseInt or parseFloat for numeric params. For production, implement a ValidationPipe with transform: true to automatically cast types.
6. Ephemeral State Assumption
- Explanation: Treating in-memory arrays as persistent storage during development.
- Impact: Data loss on server restart. Developers may write code that assumes data survives across requests, leading to bugs when switching to a database.
- Fix: Acknowledge that in-memory storage is volatile. Design services to be swappable. Write tests that seed data explicitly rather than relying on previous request state.
7. Singleton State Contamination
- Explanation: Storing request-specific data in service properties instead of passing it as arguments.
- Impact: Race conditions in concurrent requests. Data from one user leaks to another.
- Fix: Services should be stateless regarding request context. Pass all request data as method arguments. Use request-scoped providers only when absolutely necessary.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Prototype / MVP | In-Memory Service | Zero infrastructure cost. Fast iteration. Validates API design immediately. | Low |
| Production / Multi-User | Database Service | Persistence, concurrency, and data integrity. Required for reliability. | Medium (DB ops) |
| Simple Data Shape | Interface DTO | Lightweight. No runtime overhead. Sufficient for internal APIs. | Low |
| External API / Validation | Class DTO | Runtime type checking. Enables class-validator. Better error messages. | Low |
| Small App | Root Module | Simpler structure. Less boilerplate. Easier for small teams. | Low |
| Large App | Feature Modules | Encapsulation. Lazy loading. Clear boundaries. Scalable organization. | Medium (Complexity) |
Configuration Template
Use this main.ts template to bootstrap a production-ready application with global validation and error handling.
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { InventoryModule } from './inventory/inventory.module';
async function bootstrap() {
const app = await NestFactory.create(InventoryModule);
// Global Validation Pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
// Global Prefix
app.setGlobalPrefix('api/v1');
// Enable CORS
app.enableCors();
await app.listen(3000);
console.log('Application is running on: http://localhost:3000');
}
bootstrap();
Quick Start Guide
- Install CLI: Run
npm i -g @nestjs/cli.
- Create Project: Execute
nest new inventory-api and select your package manager.
- Generate Components:
nest g module inventory
nest g controller inventory
nest g service inventory
- Implement Code: Copy the DTO, Service, Controller, and Module code from the Core Solution into the generated files.
- Run: Execute
npm run start:dev to start the development server with hot reload.