Introduction

Upgrading a framework in an existing application is different from starting a new project. A new project can use the latest defaults, but an existing application may depend on older packages, custom configurations, testing tools, and third-party integrations.

NestJS 12 introduces several changes that developers should understand before upgrading. These include ESM-only framework packages, TypeScript 6 support, changes to the NestJS CLI, Standard Schema validation, GraphQL configuration updates, and improvements to application observability.

The good news is that upgrading does not mean rewriting the entire application. Existing CommonJS applications can continue working with NestJS 12, and the framework provides an upgrade command to handle several dependency and configuration changes.

However, some changes require manual attention. Applications using custom health indicators, NATS messaging, GraphQL subscriptions, or specific dependency injection patterns should be reviewed carefully.

In this article, we will upgrade an existing NestJS application, examine the important changes, update affected code, and prepare the application for deployment.

Understanding the Important Changes in NestJS 12

Before changing dependencies, it is worth understanding what has changed and which parts of an existing application might be affected.

Area

What changes

Impact on existing applications

Module system

NestJS packages are ESM-only

Check Node.js and custom tooling compatibility

TypeScript

TypeScript 6 support

Review compiler configuration and dependencies

Validation

Standard Schema support

Existing class-validator applications can continue working

Configuration

Standard Schema support in ConfigModule

Review Joi validation options

GraphQL

GraphiQL replaces the previous playground configuration

Update GraphQL settings

Microservices

NATS package changes

Review custom serializers and deserializers

Terminus

Legacy health indicator API removed

Update custom health indicators

Build tools

Rspack becomes the default for monorepos

Review custom Webpack configurations

Observability

Native NestJS observability SDK

Existing monitoring can be evaluated for integration

These changes do not affect every project in the same way. For example, an application that uses REST APIs and PostgreSQL may not need any GraphQL or NATS migration work.

The first step should always be identifying which features your application actually uses.

Checking the Environment Before Upgrading

NestJS 12 has specific runtime and development-tool requirements.

For running an application, the minimum supported Node.js versions are:

  • Node.js 20.19 or later in the 20.x line.

  • Node.js 22.12 or later in the 22.x line.

The NestJS CLI migration tools have a higher requirement:

  • Node.js 22.22.3 or later.

  • Node.js 24.15 or later.

  • Node.js 26 or later.

This distinction is important. An application may run successfully on Node.js 20.19, but the upgrade command requires a newer Node.js version.

Check your current environment:

Bash
node --version
npm --version
npx tsc --version

Also check the installed NestJS packages:

Bash
npm ls @nestjs/core @nestjs/common @nestjs/platform-express

For the migration, use TypeScript 6 because the NestJS 12 schematics require it.

If your project uses an older version, review the compatibility of testing and linting dependencies before updating TypeScript.

Upgrading an Existing NestJS Application

The recommended approach is to use the NestJS CLI upgrade command rather than manually updating every framework package.

Step 1: Create a Separate Branch

Start by creating a dedicated branch:

Bash
git checkout -b upgrade-nestjs-12

Make sure your existing application builds and its tests pass before making changes.

Bash
npm install
npm run build
npm test

Save the current state in Git. This gives you a clear comparison if something stops working after the upgrade.

Step 2: Upgrade the NestJS CLI

The upgrade command is provided by the NestJS CLI.

If you use a global installation, run:

Bash
npm install -g @nestjs/cli@latest

If your project keeps the CLI as a development dependency, update the local packages:

Bash
npm install -D @nestjs/cli@latest @nestjs/schematics@latest typescript@^6

If npm reports dependency conflicts involving ts-jest or typescript-eslint, check their installed versions and update them to releases compatible with TypeScript 6.

Step 3: Preview the Upgrade

Run the following command from the root directory of your project:

Bash
nest upgrade --dry-run

The dry-run option lets you review the proposed migration without applying the changes.

Check the report for:

  • Packages that will be upgraded.

  • Configuration files that will be modified.

  • Deprecated options.

  • Changes requiring manual code updates.

If the proposed changes look correct, run:

Bash
nest upgrade

The command updates recognized NestJS packages together and applies several mechanical migration changes.

It does not automatically solve every application-specific compatibility problem. Review the generated report and inspect the modified files.

Step 4: Verify the Installed Packages

After upgrading, check the framework packages:

Bash
npm ls @nestjs/core @nestjs/common @nestjs/platform-express

Make sure the installed packages use compatible version 12 releases.

Then rebuild the application:

Bash
npm run build

Do not move directly to deployment just because the installation and build commands succeed. The next step is to review the behavioral changes.

Understanding the ESM Changes

One of the biggest changes in NestJS 12 is that the core NestJS packages are distributed as ECMAScript modules (ESM).

Earlier NestJS projects commonly used CommonJS.

For example, an existing application may have this configuration:

JSON
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2022",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Developers may assume that they must change the entire project to ESM before upgrading. That is not necessary.

NestJS 12 can work with existing CommonJS applications through modern Node.js support for loading ESM using require(esm).

You can continue using your existing module format if your application and its tooling are compatible.

If you decide to migrate your own application to ESM, add the following property to package.json:

JSON
{
  "type": "module"
}

Review your TypeScript configuration as well:

JSON
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

In an ESM project, relative imports in generated JavaScript may require .js extensions.

For example:

TypeScript
import { UsersService } from './users.service.js';

Do not change your module system just to complete the upgrade. First check your build scripts, testing framework, and deployment environment.

Updating Validation and Configuration

NestJS 12 adds first-class support for Standard Schema-compatible validation libraries.

Existing applications using class-validator and class-transformer do not need to replace their validation implementation.

For example, the traditional DTO approach continues to work:

TypeScript
import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;
}

You can continue using the existing ValidationPipe.

For applications that prefer schema-based validation, NestJS 12 provides StandardSchemaValidationPipe.

For example, a Zod schema can describe the expected request:

TypeScript
import { z } from 'zod';

export const createUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
});

This approach is useful when validation rules are already defined as schemas and need to be reused across different parts of an application.

Updating ConfigModule Validation

The configuration module also supports Standard Schema.

For example, an application can validate environment variables with Zod:

TypeScript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { z } from 'zod';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: z.object({
        NODE_ENV: z
          .enum(['development', 'production', 'test'])
          .default('development'),

        PORT: z.coerce.number().default(3000)
      })
    })
  ]
})
export class AppModule {}

This checks environment variables when the application starts.

It helps identify missing or invalid configuration before the application begins handling requests.

If your existing application uses Joi, it can continue working with Joi 18 or later. However, library-specific options previously passed directly under validationOptions must now be placed inside validationOptions.libraryOptions.

Reviewing GraphQL and Microservice Configurations

Applications using GraphQL should review their IDE configuration.

GraphiQL is now the default GraphQL IDE. The previous playground configuration has been replaced.

For example:

TypeScript
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  graphiql: {
    url: '/graphql',
    headers: {
      authorization: 'Bearer token'
    },
    shouldPersistHeaders: true,
    isHeadersEditorEnabled: true
  }
});

If your application uses GraphQL subscriptions, review the updated subscriptions transport configuration as well.

For NATS-based microservices, NestJS 12 introduces a package change to @nats-io/nats-core.

Applications using custom message serializers or deserializers should be checked carefully because the message handling behavior has changed.

For example, the updated package provides message headers through its own API:

TypeScript
import * as nats from '@nats-io/nats-core';
import { NatsRecordBuilder } from '@nestjs/microservices';

const headers = nats.headers();

headers.set('x-version', '1.0.0');

const record = new NatsRecordBuilder(payload)
  .setHeaders(headers)
  .build();

Custom deserializers should also be reviewed because they now receive the complete NATS message object rather than only a raw Uint8Array. The payload can be read using msg.json() when working with JSON messages.

Updating Custom Health Indicators

If your application uses @nestjs/terminus, check whether you have custom health indicators extending the old HealthIndicator class.

The legacy HealthIndicator and HealthCheckError APIs have been removed.

The updated approach uses HealthIndicatorService.

For example:

TypeScript
import { Injectable } from '@nestjs/common';
import {
  HealthIndicatorService
} from '@nestjs/terminus';

@Injectable()
export class DatabaseHealthIndicator {
  constructor(
    private readonly healthIndicatorService:
      HealthIndicatorService
  ) {}

  async check() {
    const indicator =
      this.healthIndicatorService.check('database');

    try {
      await this.checkConnection();

      return indicator.up();
    } catch {
      return indicator.down({
        message: 'Database connection failed'
      });
    }
  }

  private async checkConnection() {
    // Add the actual database connection check.
  }
}

The example shows the new indicator structure. Replace checkConnection() with your actual database connectivity check.

The important difference is that the indicator now returns an explicit healthy or unhealthy result rather than throwing the old health-check exception.

Other Changes Worth Reviewing

There are a few additional changes that can affect existing applications.

Lifecycle Hook Ordering

NestJS 12 changes lifecycle hook execution ordering across component hierarchy levels.

Review providers that depend on initialization or shutdown happening in a particular order.

Avoid relying on the order of unrelated providers unless the dependency is explicitly managed.

Optional Dependencies in Inherited Constructors

If a child class declares its own constructor, it must declare @Optional() again for an optional dependency inherited from its parent.

For example:

TypeScript
import {
  Injectable,
  Optional
} from '@nestjs/common';

@Injectable()
export class BaseService {
  constructor(
    @Optional() protected options?: object
  ) {}
}

@Injectable()
export class ChildService extends BaseService {
  constructor(
    @Optional() options?: object
  ) {
    super(options);
  }
}

Without the decorator in the child constructor, NestJS may treat the dependency as required.

Structured Logging

NestJS 12 changes how ConsoleLogger handles plain objects passed after a log message.

For example:

TypeScript
logger.log('User created', {
  userId: 101,
  email: '[email protected]'
});

The object is treated as structured parameters attached to the log entry rather than as a separate log record.

If your logging system depends on the previous behavior, review the structuredParams option.

Observability

NestJS 12 introduces the official @nestjs/observe SDK.

It provides visibility into application activity through NestJS components such as controllers, providers, GraphQL resolvers, and queue consumers.

This can help teams understand where requests spend time and investigate application failures.

It is an additional capability, not a requirement for upgrading. Existing monitoring solutions can remain in place while the team evaluates whether native NestJS instrumentation is useful.

Advantages and Disadvantages of Upgrading

Advantages

Disadvantages

Updated framework architecture

Requires newer Node.js versions

Standard Schema support

Existing configuration may need changes

Improved observability integration

Monitoring setup requires evaluation

Updated CLI migration tools

Custom build configurations need review

Better ESM support

Older testing tools may create compatibility problems

Updated microservice integrations

Custom serializers may require changes

The main benefit is that developers can adopt the newer framework capabilities without rebuilding their applications from scratch.

The main challenge is identifying application-specific behavior that cannot be migrated automatically.

Common Mistakes During Migration

Upgrading Only @nestjs/core

NestJS applications use several related packages. Updating only the core package can create incompatible dependency versions.

Use the upgrade command and review the complete package list.

Assuming Every Application Must Use ESM

ESM-only framework packages do not mean that every existing application must change its module system. CommonJS applications can continue working with a compatible Node.js runtime.

Ignoring Testing Framework Compatibility

Jest can load the ESM-only NestJS 12 packages only on Node.js 24.9 or later. Older supported application runtimes may therefore need a different testing setup, such as Vitest.

Skipping Integration Tests

A successful build does not prove that GraphQL, NATS, authentication, or database connections are working correctly.

Test the integrations that your application actually uses.

Deploying Without a Rollback Plan

Keep the previous working release available until the upgraded application has been verified in production.

Best Practices for a Safe Upgrade

  1. Create a separate branch for the migration.

  2. Verify Node.js and TypeScript compatibility.

  3. Run the upgrade command with --dry-run before applying changes.

  4. Upgrade related NestJS packages together.

  5. Review custom GraphQL, NATS, and health-check implementations.

  6. Keep the existing module system unless there is a clear reason to change it.

  7. Run unit, integration, and end-to-end tests.

  8. Verify environment variables and application configuration.

  9. Deploy to staging before production.

  10. Keep a tested rollback plan ready.

Summary

Upgrading an existing application to NestJS 12 requires attention to more than package versions. The framework introduces ESM-only packages, TypeScript 6 support, Standard Schema validation, updated CLI workflows, and changes to several integrations.

The nest upgrade command handles many mechanical migration tasks, but developers still need to review application-specific code. In particular, custom health indicators, NATS deserializers, GraphQL configuration, lifecycle hooks, and testing environments deserve attention.

Existing CommonJS applications do not need a complete ESM rewrite. Similarly, applications using class-validator can continue with their current validation approach.

The best way to approach this upgrade is to make changes in a separate branch, review the migration report, test every important integration, and verify the application in staging before production deployment.

This approach keeps the migration controlled and helps developers take advantage of NestJS 12 without introducing unnecessary changes to an existing application.