---
# === IDENTITY ===
id: software/migrations/express-to-nestjs/2026
canonical_question: "How do I migrate from Express.js to NestJS?"
aliases:
  - "Express to NestJS migration guide"
  - "convert Express app to NestJS"
  - "migrate Express.js to Nest"
  - "rewrite Express backend in NestJS"
  - "Express.js to NestJS upgrade"
  - "move Express API to NestJS"
  - "Express to NestJS step by step"
  - "NestJS migration from Express"
entity_type: software_reference
domain: software > migrations > express_to_nestjs
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.93
freshness: evolving
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "NestJS 11 (2025-01) — Express 5 default, named wildcards, reversed lifecycle hooks. NestJS 12 (target Q3 2026) — full ESM, Vitest, oxlint, Rspack."
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "NestJS 11 requires Node.js v20+ — v16 and v18 support was dropped"
  - "Express 5 requires named route wildcards (/*splat) — unnamed /* will throw at startup"
  - "Never mix @Res() manual response with return values — it bypasses interceptors, filters, and serialization"
  - "emitDecoratorMetadata and experimentalDecorators must be true in tsconfig.json — DI silently fails without them"
  - "Multer (FileInterceptor) has compatibility issues with Express 5 — verify multer version supports Express 5 before upgrading"
  - "Express 5 no longer uses qs for query parsing — nested objects/arrays in query strings require manual query parser config"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to migrate Express to Fastify (not NestJS)"
    use_instead: "software/migrations/express-to-fastify/2026"
  - condition: "User wants the dependency injection pattern itself, not a NestJS migration"
    use_instead: "software/patterns/dependency-injection/2026"
  - condition: "User wants to migrate a non-Node monolith to microservices"
    use_instead: "software/migrations/monolith-to-microservices/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: typescript_status
    question: "Is the existing Express codebase already in TypeScript?"
    type: choice
    options: ["Yes — TypeScript", "No — JavaScript only"]
  - key: codebase_scale
    question: "How large is the Express codebase?"
    type: choice
    options: ["Small (<10 routes)", "Medium (10-50 routes)", "Large (>50 routes)"]
  - key: migration_strategy
    question: "Do you prefer incremental migration or full rewrite?"
    type: choice
    options: ["Incremental (hybrid server)", "Full rewrite"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/express-to-nestjs/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/patterns/dependency-injection/2026"
      label: "Dependency Injection Pattern"
  related_to:
    - id: "software/migrations/express-to-fastify/2026"
      label: "Express.js to Fastify Migration"
    - id: "software/migrations/rest-to-graphql/2026"
      label: "REST to GraphQL Migration"
    - id: "software/migrations/monolith-to-microservices/2026"
      label: "Monolith to Microservices Migration"
  alternative_to:
    - id: "software/migrations/express-to-fastify/2026"
      label: "Express.js to Fastify Migration"
  often_confused_with:
    - id: "software/migrations/express-to-fastify/2026"
      label: "Express.js to Fastify Migration (similar starting point, different target framework)"

# === SOURCES (10 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "NestJS Documentation — First Steps"
    author: NestJS
    url: https://docs.nestjs.com/
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src2
    title: "Efficiently Migrate from Express to NestJS: A Step-by-Step Guide"
    author: Delving Developer
    url: https://delvingdeveloper.com/posts/migrating-from-express-to-nestjs
    type: technical_blog
    published: 2025-08-20
    reliability: high
  - id: src3
    title: "Announcing NestJS 11: What's New"
    author: Trilon
    url: https://trilon.io/blog/announcing-nestjs-11-whats-new
    type: technical_blog
    published: 2025-12-10
    reliability: high
  - id: src4
    title: "Migrating from Express to NestJS: Developer Insights"
    author: Delving Developer
    url: https://delvingdeveloper.com/posts/migrating-from-express-to-nestjs-developers-experience
    type: technical_blog
    published: 2025-09-15
    reliability: high
  - id: src5
    title: "NestJS vs Express — Better Stack Community"
    author: Better Stack
    url: https://betterstack.com/community/guides/scaling-nodejs/nestjs-vs-express/
    type: community_resource
    published: 2025-10-01
    reliability: moderate_high
  - id: src6
    title: "NestJS Middleware Documentation"
    author: NestJS
    url: https://docs.nestjs.com/middleware
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src7
    title: "Express 5.1.0: Now the Default on npm with LTS Timeline"
    author: Express.js Team
    url: https://expressjs.com/2025/03/31/v5-1-latest-release.html
    type: official_docs
    published: 2025-03-31
    reliability: authoritative
  - id: src8
    title: "From Express to NestJS Without the Pain"
    author: Syntal
    url: https://medium.com/@sparknp1/from-express-to-nestjs-without-the-pain-f2c0ed873897
    type: technical_blog
    published: 2026-01-15
    reliability: moderate_high
  - id: src9
    title: "NestJS v12 Roadmap: Full ESM Migration, Standard Schema Validation and Modernised Toolchain"
    author: InfoQ
    url: https://www.infoq.com/news/2026/04/nestjs-12-roadmap-esm/
    type: industry_report
    published: 2026-04-01
    reliability: high
  - id: src10
    title: "Express 3 is EOL, Express 4 is Next: The 2026 Support Reference"
    author: HeroDevs
    url: https://www.herodevs.com/blog-posts/express-3-is-eol-express-4-is-next-the-2026-support-reference
    type: industry_report
    published: 2026-01-10
    reliability: high
---

# How to Migrate from Express.js to NestJS

## TL;DR

- **Bottom line**: Migrate incrementally — install NestJS alongside Express using `ExpressAdapter`, convert route handlers to controllers with decorators, extract business logic into injectable services, replace middleware with guards/pipes/interceptors where appropriate, and adopt NestJS modules to organize your codebase.
- **Key tool/command**: `npx @nestjs/cli new my-project` (scaffold) or `nest generate resource users` (full CRUD per-resource migration)
- **Watch out for**: Dumping all Express middleware into NestJS middleware classes — use guards for auth, pipes for validation, and interceptors for logging/transforms instead. Also: Express 5 is now the default in NestJS 11, requiring named route wildcards (`/*splat`).
- **Works with**: NestJS 11.x (current; NestJS 12 targeting Q3 2026 brings ESM + Vitest + oxlint + Rspack), Express 5.2.x (default, shipped Dec 2025) / Express 4.x (maintenance until ~Oct 2026), Node.js 20+, TypeScript 5.x.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Node.js v20+ required**: NestJS 11 dropped support for Node.js v16 and v18. Verify with `node -v` before starting migration. [src3]
- **Named route wildcards mandatory**: Express 5 requires `/*splat` or `/{*splat}` instead of `/*`. Unnamed wildcards throw at startup. Run `@expressjs/codemod` to automate this change. [src3, src7]
- **Never mix `@Res()` with return values**: Using `@Res()` and calling `res.json()` bypasses interceptors, exception filters, and serialization. Only use `@Res()` for streaming or file downloads. [src1, src4]
- **tsconfig decorators required**: Set `"emitDecoratorMetadata": true` and `"experimentalDecorators": true` in `tsconfig.json` — NestJS DI silently injects `undefined` without them. [src2]
- **Multer + Express 5 compatibility**: `FileInterceptor` uses multer under the hood. Verify your multer version supports Express 5 before upgrading — incompatible versions silently fail on multipart uploads. [src3]
- **Query string parsing changed**: Express 5 no longer uses the `qs` library by default — nested objects and arrays in query strings break. Configure `app.set('query parser', 'extended')` if needed. [src7]

## Quick Reference

| Express Pattern | NestJS Equivalent | Example |
|---|---|---|
| `app.get('/users', handler)` | `@Controller('users')` + `@Get()` | `@Get() findAll() { return this.usersService.findAll(); }` |
| `app.post('/users', handler)` | `@Post()` decorator | `@Post() create(@Body() dto: CreateUserDto) { ... }` |
| `req.params.id` | `@Param('id')` decorator | `@Get(':id') findOne(@Param('id') id: string)` |
| `req.query.limit` | `@Query('limit')` decorator | `@Get() findAll(@Query('limit') limit: number)` |
| `req.body` | `@Body()` decorator | `@Post() create(@Body() dto: CreateUserDto)` |
| `app.use(cors())` | `app.enableCors()` | `app.enableCors({ origin: 'https://example.com' })` |
| `app.use(helmet())` | `app.use(helmet())` | Same — NestJS reuses Express middleware directly |
| `app.use(authMiddleware)` | `@UseGuards(AuthGuard)` | `@UseGuards(JwtAuthGuard) @Get('profile') getProfile()` |
| `express.Router()` | `@Module()` + `@Controller()` | Each feature module groups related controllers and services |
| `app.use(express.json())` | Built-in (auto-enabled) | Body parsing included by default in NestJS |
| `app.use(morgan('dev'))` | `@UseInterceptors(LoggingInterceptor)` | `intercept(context, next) { ... return next.handle().pipe(tap(...)) }` |
| `express-validator` | `class-validator` + `ValidationPipe` | `@IsString() @MinLength(3) name: string;` in DTO |
| `try/catch in handler` | `@UseFilters(HttpExceptionFilter)` | Exception filters catch and format errors globally |
| `app.listen(3000)` | `await app.listen(3000)` | `const app = await NestFactory.create(AppModule); await app.listen(3000);` |
| `module.exports = router` | `@Module({ controllers, providers })` | Modules encapsulate feature boundaries with DI |

## Decision Tree

```
START
├── Is the Express codebase in TypeScript already?
│   ├── YES → Faster migration — skip TS conversion, focus on architecture
│   └── NO → Convert to TypeScript first OR migrate to NestJS + TS simultaneously ↓
├── Is the Express app using Express 4 or Express 5?
│   ├── Express 4 → NestJS 11 supports both; can keep Express 4 as platform initially
│   └── Express 5 → Update route wildcards to /*splat syntax before migrating ↓
├── Is it a monolith with >20 route files?
│   ├── YES → Migrate module-by-module (users, auth, products, etc.)
│   └── NO ↓
├── Does the app use Express middleware for auth/validation/logging?
│   ├── YES → Replace auth middleware with Guards, validation with Pipes, logging with Interceptors
│   └── NO ↓
├── Does the app use an ORM (Sequelize, Knex, Prisma, TypeORM)?
│   ├── YES → Wrap ORM in NestJS providers, keep existing queries
│   └── NO ↓
├── Does the app use multer for file uploads?
│   ├── YES → Verify multer version supports Express 5, then use FileInterceptor
│   └── NO ↓
├── Are there WebSocket or microservice requirements?
│   ├── YES → Use NestJS Gateways (@WebSocketGateway) and Microservices module
│   └── NO ↓
└── DEFAULT → Scaffold NestJS project, migrate route-by-route into controllers + services
```

## Step-by-Step Guide

### 1. Scaffold a new NestJS project alongside Express

Create a fresh NestJS project. You can run it in parallel with your Express app during migration — both can coexist serving different routes behind a reverse proxy or on the same server using `ExpressAdapter`. [src1, src2]

```bash
npm i -g @nestjs/cli
nest new my-nestjs-app
cd my-nestjs-app
npm run start:dev
```

**Verify**: `curl http://localhost:3000` → returns `"Hello World!"`. NestJS is running on port 3000.

### 2. Convert Express route handlers to NestJS controllers

Take an Express route group (e.g., `/users`) and create the equivalent NestJS controller. Each `app.get()`, `app.post()`, etc. becomes a decorated method. [src2, src4]

```typescript
// BEFORE: Express routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', async (req, res) => {
  const users = await db.query('SELECT * FROM users');
  res.json(users);
});

router.get('/:id', async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
  if (!user) return res.status(404).json({ error: 'Not found' });
  res.json(user);
});

router.post('/', async (req, res) => {
  const { name, email } = req.body;
  const user = await db.query('INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *', [name, email]);
  res.status(201).json(user);
});

module.exports = router;

// AFTER: NestJS users.controller.ts
import { Controller, Get, Post, Param, Body, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    const user = await this.usersService.findOne(id);
    if (!user) throw new NotFoundException('User not found');
    return user;
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}
```

**Verify**: `nest generate controller users` scaffolds the file. `curl http://localhost:3000/users` returns data.

### 3. Extract business logic into NestJS services

Move database queries and business logic out of controllers into injectable services. This is the core architectural improvement over Express — controllers become thin, services are testable and reusable. [src4, src5]

```typescript
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<User | null> {
    return this.usersRepository.findOneBy({ id });
  }

  async create(dto: CreateUserDto): Promise<User> {
    const user = this.usersRepository.create(dto);
    return this.usersRepository.save(user);
  }
}
```

**Verify**: `nest generate service users` scaffolds the file. Write a unit test: `npm run test -- --testPathPattern=users.service`.

### 4. Group related components into NestJS modules

Every feature gets its own module that declares its controllers, providers (services), and imports. This replaces Express's flat `routes/` directory. [src1, src2]

```typescript
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // makes UsersService available to other modules
})
export class UsersModule {}

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: 5432,
      database: process.env.DB_NAME,
      autoLoadEntities: true,
    }),
    UsersModule,
  ],
})
export class AppModule {}
```

**Verify**: `nest generate module users`. App compiles and all routes under `/users` work.

### 5. Replace Express middleware with NestJS guards, pipes, and interceptors

Express middleware handles everything (auth, validation, logging, error handling). NestJS splits these concerns into purpose-built constructs. [src4, src6]

```typescript
// BEFORE: Express auth middleware
function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Unauthorized' });
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}
app.use('/api', authMiddleware);

// AFTER: NestJS auth guard
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    if (!token) throw new UnauthorizedException();
    try {
      request.user = this.jwtService.verify(token);
      return true;
    } catch {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

// Apply globally or per-controller
@UseGuards(JwtAuthGuard)
@Controller('users')
export class UsersController { ... }
```

**Verify**: `curl -H "Authorization: Bearer invalid" http://localhost:3000/users` → 401 Unauthorized.

### 6. Migrate validation from Express middleware to NestJS pipes + DTOs

Replace `express-validator` or manual `req.body` checks with class-validator decorators and the global ValidationPipe. [src1, src5]

```typescript
// BEFORE: Express validation
const { body, validationResult } = require('express-validator');
router.post('/users',
  body('email').isEmail(),
  body('name').isLength({ min: 3 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
    // ... create user
  }
);

// AFTER: NestJS DTO + ValidationPipe
// dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  name: string;

  @IsEmail()
  email: string;
}

// main.ts — enable globally
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,       // strips unknown properties
  forbidNonWhitelisted: true,  // throws on unknown properties
  transform: true,       // auto-transform payloads to DTO instances
}));
```

**Verify**: `curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"ab","email":"bad"}' ` → 400 with validation errors.

### 7. Migrate error handling to NestJS exception filters

Replace Express `app.use((err, req, res, next))` error handlers with NestJS exception filters that catch specific exception types. [src1, src4]

```typescript
// BEFORE: Express error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
});

// AFTER: NestJS exception filter
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;
    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal Server Error';

    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
    });
  }
}

// main.ts — apply globally
app.useGlobalFilters(new AllExceptionsFilter());
```

**Verify**: Throw an error in any controller method — the filter catches and formats it consistently.

### 8. Update route wildcards for Express 5 compatibility

If migrating to NestJS 11 with Express 5 (the new default), update all wildcard route patterns. Express 5 requires named wildcards. Use the official codemod to automate most changes. [src3, src7]

```bash
# Run the official Express codemod to automate route pattern updates
npx @expressjs/codemod .

# Manual updates needed for NestJS decorators:
```

```typescript
// BEFORE: Express 4 wildcard routes
app.get('/files/*', serveFiles);           // unnamed wildcard
app.get('/api/:version/*', proxyHandler);  // unnamed wildcard

// AFTER: Express 5 / NestJS 11 named wildcards
@Get('files/*splat')                       // named wildcard
serveFiles(@Param('splat') path: string) { ... }

@Get('api/:version/*splat')                // named wildcard
proxyHandler(@Param('version') version: string, @Param('splat') path: string) { ... }
```

**Verify**: `npx nest info` shows NestJS 11.x. All routes with wildcards load without startup errors.

## Code Examples

### TypeScript: Complete Express-to-NestJS controller migration

> Full script: [typescript-complete-express-to-nestjs-controller-m.ts](scripts/typescript-complete-express-to-nestjs-controller-m.ts) (58 lines)

```typescript
// Input:  Express route file with CRUD operations, middleware, and validation
// Output: Equivalent NestJS module with controller, service, DTO, and guard
// --- Express original (routes/products.js) ---
// const router = express.Router();
// router.get('/', async (req, res) => { ... });
# ... (see full script)
```

### TypeScript: Migrating Express middleware to NestJS interceptor

```typescript
// Input:  Express response-time + logging middleware
// Output: NestJS interceptor with execution context and RxJS observable

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const startTime = Date.now();

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const duration = Date.now() - startTime;
        this.logger.log(`${method} ${url} ${response.statusCode} - ${duration}ms`);
      }),
    );
  }
}

// Apply globally in main.ts:
// app.useGlobalInterceptors(new LoggingInterceptor());
```

### TypeScript: Running Express and NestJS side-by-side during migration

```typescript
// Input:  Existing Express app that needs gradual migration
// Output: NestJS app mounted alongside Express using the Express adapter

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import { AppModule } from './app.module';

async function bootstrap() {
  const server = express();

  // Legacy Express routes still work
  server.get('/legacy/health', (req, res) => {
    res.json({ status: 'ok', framework: 'express' });
  });

  // Mount NestJS on the same Express instance
  const app = await NestFactory.create(AppModule, new ExpressAdapter(server));
  await app.init();

  // NestJS routes (/users, /products, etc.) coexist with legacy routes
  server.listen(3000, () => {
    console.log('Hybrid Express + NestJS server running on port 3000');
  });
}

bootstrap();
```

### TypeScript: Configuring Express 5 query parser in NestJS 11

```typescript
// Input:  NestJS 11 app that needs nested query string support (Express 5 default changed)
// Output: NestJS bootstrap with explicit query parser configuration

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // Express 5 dropped qs by default — restore nested query support
  app.set('query parser', 'extended');

  // Enable global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));

  await app.listen(3000);
}

bootstrap();
```

## Anti-Patterns

### Wrong: Putting all business logic in NestJS controllers

```typescript
// ❌ BAD — Controller does everything, same problem as Express route handlers
@Controller('orders')
export class OrdersController {
  @Post()
  async create(@Body() body: any) {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [body.userId]);
    if (!user) throw new NotFoundException();
    const order = await db.query('INSERT INTO orders ...', [body]);
    await emailService.send(user.email, 'Order confirmed');
    await inventoryService.decrement(body.items);
    return order;
  }
}
```

### Correct: Thin controllers, fat services

```typescript
// ✅ GOOD — Controller delegates to service, service is testable and reusable
@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Post()
  create(@Body() dto: CreateOrderDto) {
    return this.ordersService.create(dto);
  }
}

@Injectable()
export class OrdersService {
  constructor(
    private readonly usersService: UsersService,
    private readonly emailService: EmailService,
    private readonly inventoryService: InventoryService,
  ) {}

  async create(dto: CreateOrderDto): Promise<Order> {
    const user = await this.usersService.findOneOrFail(dto.userId);
    const order = await this.ordersRepository.save(dto);
    await this.emailService.send(user.email, 'Order confirmed');
    await this.inventoryService.decrement(dto.items);
    return order;
  }
}
```

### Wrong: Using NestJS middleware for everything (just like Express)

```typescript
// ❌ BAD — Treating NestJS middleware the same as Express middleware
@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // auth logic here
  }
}
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // validation logic here
  }
}
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // logging logic here
  }
}
```

### Correct: Use the right NestJS construct for each concern

```typescript
// ✅ GOOD — Guards for auth, Pipes for validation, Interceptors for logging
@UseGuards(JwtAuthGuard)        // Auth → Guard (has access to ExecutionContext)
@UsePipes(ValidationPipe)       // Validation → Pipe (transforms + validates DTOs)
@UseInterceptors(LoggingInterceptor)  // Logging → Interceptor (wraps handler with RxJS)
@Controller('orders')
export class OrdersController { ... }
```

### Wrong: Migrating everything at once (big-bang rewrite)

```typescript
// ❌ BAD — Rewriting the entire Express app before shipping anything
// 6 months of parallel development, no production feedback, high regression risk
// "We'll migrate everything, then switch over on launch day"
```

### Correct: Incremental migration with hybrid server

```typescript
// ✅ GOOD — Express and NestJS coexist on the same server
// Migrate one module at a time, deploy after each module
const server = express();
// Legacy Express routes
server.use('/api/v1/legacy', legacyRouter);
// New NestJS modules handle migrated routes
const app = await NestFactory.create(AppModule, new ExpressAdapter(server));
await app.init();
// Route /api/v1/users → NestJS (migrated)
// Route /api/v1/legacy/reports → Express (not yet migrated)
```

### Wrong: Not using dependency injection

```typescript
// ❌ BAD — Importing and instantiating services directly (Express habit)
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  private usersService = new UsersService();  // manual instantiation

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}
```

### Correct: Let NestJS DI container manage instances

```typescript
// ✅ GOOD — Constructor injection, testable, lifecycle-managed
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}
// UsersService is declared as a provider in UsersModule
// NestJS creates and injects the singleton automatically
```

## Common Pitfalls

- **Forgetting to register modules in AppModule**: Every NestJS module must be imported in `AppModule` or another parent module. Unregistered modules silently fail — controllers return 404 with no error. Fix: Always add new modules to `AppModule.imports`. [src1]
- **Using `any` types instead of DTOs**: Migrating from Express, developers often skip DTO creation and use `@Body() body: any`. This loses the main benefit of NestJS — type safety and automatic validation. Fix: Create a DTO class with `class-validator` decorators for every request body. [src2]
- **Circular dependency between modules**: Two modules importing each other causes `undefined` at runtime. This happens when migrating tightly-coupled Express code. Fix: Use `forwardRef(() => OtherModule)` or restructure into a shared module. [src1]
- **Not enabling `emitDecoratorMetadata` in tsconfig**: NestJS DI relies on TypeScript decorator metadata. Without this flag, injected dependencies are `undefined`. Fix: Set `"emitDecoratorMetadata": true` and `"experimentalDecorators": true` in `tsconfig.json`. [src2]
- **Mixing Express `res.json()` with NestJS return values**: In NestJS, controllers return objects that get serialized automatically. Using `@Res()` and calling `res.json()` bypasses interceptors, exception filters, and serialization. Fix: Return objects from controller methods; only use `@Res()` for streaming or file downloads. [src1, src4]
- **Route wildcards breaking on NestJS 11 + Express 5**: Express 5 requires named wildcards (`/*splat`) instead of unnamed (`/*`). Fix: Update route patterns to use `@Get('files/*splat')` syntax. Run `npx @expressjs/codemod .` to automate most changes. [src3, src7]
- **Not running both servers during migration**: Attempting to migrate the entire Express app before deploying anything introduces high risk. Fix: Use `ExpressAdapter` to mount NestJS on the same Express instance and migrate one module at a time. [src8]
- **Query strings breaking with Express 5**: Nested objects in query parameters (e.g., `?filter[name]=foo`) stop working because Express 5 dropped `qs` as default parser. Fix: Add `app.set('query parser', 'extended')` in bootstrap. [src7]

## Diagnostic Commands

```bash
# Scaffold a new NestJS project
npx @nestjs/cli new my-project

# Generate a full CRUD resource (controller + service + module + DTOs)
nest generate resource users

# Check NestJS version and environment info
npx nest info

# Run in debug mode to see DI container resolution
nest start --debug --watch

# Run all unit tests
npm run test

# Run end-to-end tests
npm run test:e2e

# Check for circular dependencies (NestJS will warn in console)
# Look for: "A circular dependency has been detected"

# Run Express codemod to update route patterns for Express 5
npx @expressjs/codemod .

# Verify Node.js version meets NestJS 11 requirement (v20+)
node -v
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| NestJS 12 (target Q3 2026) | Roadmap (`next` tag) | Full ESM (all official packages), Vitest replaces Jest, oxlint replaces ESLint, Rspack replaces Webpack, Standard Schema (Zod/Valibot/ArkType) in route decorators, NATS v3 in microservices, graceful shutdown on Express adapter | Test under the `next` tag; ESM transition is low-friction because Node.js now allows CJS to `require()` ESM. CJS project generation will remain available in the CLI |
| NestJS 11 (2025) | Current | Express v5 default, named wildcards (`/*splat`), reversed lifecycle hook order, cache-manager v6, Node.js 20+ required, multer compatibility issues | Update route wildcards, check `OnModuleDestroy` order, verify multer version, update query parser config |
| NestJS 10 (2023) | LTS | SWC as default compiler, new `@nestjs/cache-manager` | Replace deprecated cache module import |
| NestJS 9 (2022) | Maintenance | REPL support, configurable module builder, durable providers | Minor API changes |
| NestJS 8 (2021) | EOL | Standalone app API, lazy-loading modules | Upgrade to 10+ recommended |
| Express 5.2 (Dec 2025) | Current (Active) | Same v5 breaking changes (named wildcards, `qs` dropped, `req.query` immutable, deprecated methods removed) + 5.2 patch series — 5.2.x is the current production-recommended release | Use `/*splat`, configure query parser, run `@expressjs/codemod` |
| Express 5.1 (Mar 2025) | Superseded | Initial `latest`-tag default on npm | Upgrade to 5.2.x |
| Express 4 (2014) | Maintenance (EOL target 2026-10) | — | Still supported as NestJS HTTP platform. Express TC frames Oct 2026 as a target, not a commitment; v4 may receive extended maintenance given its 10+ year tenure |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Express codebase has >20 routes with growing complexity | Simple API with <10 endpoints, no team growth | Keep Express |
| Team needs enforced structure and conventions | Solo developer who prefers minimal frameworks | Express + custom conventions |
| Project requires WebSockets, microservices, or GraphQL | Pure REST API with no additional protocols | Express or Fastify directly |
| Hiring — NestJS developers bring consistent patterns | Legacy Express app in maintenance-only mode | Maintain Express as-is |
| Need built-in testing support (DI makes mocking easy) | Prototyping or hackathon projects | Express + Supertest |
| Enterprise project with multiple teams contributing | Serverless functions or edge workers | Hono, Cloudflare Workers |

## Important Caveats

- NestJS uses Express as its default HTTP platform but also supports Fastify. The `@nestjs/platform-express` package is included by default — you get full Express compatibility including all Express middleware.
- NestJS 11 ships with Express 5 as the default. If using Express 5, update route wildcards to the named syntax (`/*splat`). Express 4 is still supported via explicit `@nestjs/platform-express` version pinning.
- Express 5.1 became the `latest` tag on npm as of March 2025. Express 4 enters maintenance-only mode with EOL no sooner than October 2026. Plan your migration accordingly. [src7]
- The `@nestjs/cli` generates boilerplate but does not migrate existing Express code automatically. Migration is a manual, route-by-route process. The `@expressjs/codemod` package automates route pattern updates but not architectural changes.
- NestJS adds ~15-20% overhead compared to raw Express due to the DI container, decorator metadata resolution, and module initialization. For most applications, this is negligible — but measure if latency-sensitive.
- Circular dependencies are the most common runtime error during migration. NestJS provides `forwardRef()` as a workaround, but the better fix is restructuring modules to break the cycle.
- All Express middleware still works in NestJS via `app.use()` or `consumer.apply()` in module configuration. You do not need to rewrite middleware on day one.
- The order of `OnModuleDestroy` and `OnApplicationShutdown` lifecycle hooks was reversed in NestJS 11. If your Express app relies on specific shutdown ordering, verify the new sequence. [src3]
- **NestJS 12 lands around Q3 2026** (currently on the `next` npm tag): full ESM across all official packages, Vitest replaces Jest, oxlint replaces ESLint, Rspack replaces Webpack, and Standard Schema validation (Zod / Valibot / ArkType) becomes a first-class alternative to `class-validator` in route decorators. Migration friction is expected to be low — Node.js now lets CommonJS `require()` ESM modules — but teams mid-migration should target NestJS 11 today and re-evaluate tooling defaults once v12 is GA. [src9]
- **Express 5.2.x is the current production-recommended release** (shipped Dec 2025). The Express Technical Committee targets Express 4 EOL no sooner than 2026-10-01, but explicitly calls these dates "goals, not commitments" — Express 4 may receive extended maintenance support given its decade-long tenure as the only major version. [src10]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [Dependency Injection Pattern](/software/patterns/dependency-injection/2026)
- [Express.js to Fastify Migration](/software/migrations/express-to-fastify/2026)
- [REST to GraphQL Migration](/software/migrations/rest-to-graphql/2026)
- [Monolith to Microservices Migration](/software/migrations/monolith-to-microservices/2026)
