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:
The NestJS CLI migration tools have a higher requirement:
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:
node --version
npm --version
npx tsc --version
Also check the installed NestJS packages:
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:
git checkout -b upgrade-nestjs-12
Make sure your existing application builds and its tests pass before making changes.
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:
npm install -g @nestjs/cli@latest
If your project keeps the CLI as a development dependency, update the local packages:
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:
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:
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:
npm ls @nestjs/core @nestjs/common @nestjs/platform-express
Make sure the installed packages use compatible version 12 releases.
Then rebuild the application:
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:
{
"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:
{
"type": "module"
}
Review your TypeScript configuration as well:
{
"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:
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:
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:
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:
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:
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:
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:
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:
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:
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
Create a separate branch for the migration.
Verify Node.js and TypeScript compatibility.
Run the upgrade command with --dry-run before applying changes.
Upgrade related NestJS packages together.
Review custom GraphQL, NATS, and health-check implementations.
Keep the existing module system unless there is a clear reason to change it.
Run unit, integration, and end-to-end tests.
Verify environment variables and application configuration.
Deploy to staging before production.
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.
Join the conversation! Your thoughts help the community grow.