Migration

Upgrade to v1.5.0

Everything that changes between 1.0.0 and 1.5.0, the commands that do the work, and what to check when you are done.

Breaking changes
None

The underscore methods keep working exactly as before.

Typical effort
10–20 min

One dependency bump, one codemod, one boot check.

Commands
2

A package install and nest-cli m events.

Pick your path

Where are you upgrading from?

Current versionWhat to do
1.0.0

Follow the six steps below.

0.0.x beta

Run nest-cli m run first to move the decorator imports, then follow the six steps.

New project

Nothing to migrate — start from Getting Started.

1.0.0 → 1.5.0

The upgrade, step by step

Bump the packages

Every package is released together, so they all move to 1.5.0. Install core, decorators and the adapter you actually use — swap mongoose for prisma or typeorm as needed.

$npm i @nest-extended/core@^1.5.0 @nest-extended/decorators@^1.5.0 @nest-extended/mongoose@^1.5.0

And the CLI, if you have it installed globally:

$npm i -g @nest-extended/cli@^1.5.0

Move to NestJS 12 and TypeScript 6

1.5.0 is built against NestJS 12 and TypeScript 6. Applications still on NestJS 11 will usually run, but the published types target 12 — upgrade both together.

$npm i @nestjs/common@^12 @nestjs/core@^12 @nestjs/platform-express@^12
$npm i -D typescript@^6

Add the ORM-specific NestJS integration you use — @nestjs/mongoose@^12 or @nestjs/typeorm@^12.

Pin Prisma to v7

Prisma only

Prisma 8 (“Prisma Next”) is a different product: prisma init --datasource-provider, prisma generate and prisma db push no longer exist, the ORM commands moved under prisma orm, and the client layout changed. Everything the generator emits targets Prisma 7.

$npm i @prisma/client@^7 @prisma/adapter-pg@^7
$npm i -D prisma@^7

Swap @prisma/adapter-pg for @prisma/adapter-mariadb (MySQL) or @prisma/adapter-better-sqlite3 (SQLite).

Switch controllers to the event-firing methods

This is the change that turns service events on. Each NestService now has two families: _find / _get / _create / _patch / _remove stay event-free for service-to-service calls, while find / get / create / patch / remove do the same work and dispatch events. The codemod rewrites controllers from the first family to the second.

Terminal
$cd my-app
 
# see every replacement first — nothing is written
$nest-cli m events --dry-run
 
# apply, after confirming
$nest-cli m events

The dry run prints each call site:

src/services/company/company.controller.ts
    _find -> find (line 24)
    _get -> get (line 31)
    ...

2 file(s), 10 replacement(s).

It only touches src/**/*.controller.ts by default, which is the point: an AuthService looking a user up with usersService._find(...) should not fire the users service’s onFind hook.

Before — v1.0.0
company.controller.ts
@Controller('company')
export class CompanyController {
  constructor(private readonly service: CompanyService) {}

  @Get()
  find(@Query() query: Record<string, any>) {
    return this.service._find(query);
  }

  @Post()
  create(@ModifyBody(setCreatedBy()) body: CreateCompanyDto) {
    return this.service._create(body);
  }
}
After — v1.5.0
company.controller.ts
@Controller('company')
export class CompanyController {
  constructor(private readonly service: CompanyService) {}

  @Get()
  find(@Query() query: Record<string, any>) {
    return this.service.find(query);      // fires beforeFind / onFind
  }

  @Post()
  create(@ModifyBody(setCreatedBy()) body: CreateCompanyDto) {
    return this.service.create(body);     // fires beforeCreate / onCreate
  }
}

Add an events class where you want one

Optional

Step 4 makes hooks possible; nothing fires until a service has an events class. Generate one for a new resource with nest-cli g service (it is the default — --skip-events opts out), or write it by hand for an existing one.

company.events.ts
import { Injectable } from '@nestjs/common';
import { EventContext, NestServiceEvents, ServiceEvents } from '@nest-extended/core';
import { CompanyService } from './company.service';
import { CompanyDocument } from '../../schemas/company.schema';

@Injectable()
@ServiceEvents(CompanyService)
export class CompanyEvents extends NestServiceEvents<CompanyService, CompanyDocument> {
  // Inline and awaited — what you return replaces the input.
  beforeCreate(data: any, ctx: EventContext<CompanyService>) {
    return { ...data, slug: slugify(data.name) };
  }

  // Detached — runs after the response has been sent.
  async onCreate(company: CompanyDocument, ctx: EventContext<CompanyService>) {
    await this.mailer.sendWelcome(company, ctx.user);
  }
}

Register it next to the service in the feature module’s providers — the generator does this for you:

company.module.ts
@Module({
  imports: [MongooseModule.forFeature([{ name: Company.name, schema: CompanySchema }])],
  controllers: [CompanyController],
  providers: [CompanyService, CompanyEvents],   // <- both classes
})
export class CompanyModule {}

To broadcast events to other modules as well, install @nestjs/event-emitter, register EventEmitterModule.forRoot(), and construct the service with { broadcast: 'company' }. nest-cli g service --broadcast wires all three up for you. The full hook list, the context object and the limitations are in Service Events.

Boot and verify

Start the app and look for one wiring line per events class. If a line is missing, that class is not registered or NestExtendedModule.forRoot() is absent.

[Nest] LOG [NestExtendedEvents] CompanyEvents -> CompanyService
[Nest] LOG [NestExtendedEvents] UserEvents -> UsersService

Then run your test suite. Tests that assert on an on* hook need await service.whenSettled() first, because those hooks are detached.


Reference

What changed, and what it means for you

ChangeImpactAction
Generated controllers call find / create / … instead of _find / _create / …Identical arguments and responses — the plain methods only add event dispatch.Run nest-cli m events, or leave the underscore calls and no hook fires.
NestService constructor options gained events and broadcastevents defaults to true; with no events class attached, nothing changes.Nothing. Pass { events: false } to opt a service out.
@nest-extended/core now depends on @nest-extended/decoratorsThe interceptor reads the decorators’ metadata keys.Keep both packages on the same version.
NestJS 12 and TypeScript 6The packages are built against them; NestJS 11 apps should upgrade too.Bump @nestjs/* to 12 and typescript to 6.
Prisma pinned to v7Prisma 8 removes prisma generate / db push and moves commands under prisma orm; the generated client layout targets 7.Pin @prisma/client, the driver adapter and dev prisma to ^7.
Scaffolded apps are emitted as CommonJSOnly affects apps generated from 1.5.0 onward.Nothing for existing apps.

Migration command reference

CommandAliasPurpose
nest-cli migration eventsm eventsSwitch controllers from _find / _get / … to the event-firing methods.
nest-cli m events --dry-run—Print every replacement with its file and line, write nothing.
nest-cli m events -y--yesApply without the confirmation prompt — for scripted upgrades.
nest-cli m events --path 'src/**/*.ts'—Widen the scope beyond src/**/*.controller.ts. Rarely what you want: service-to-service calls should stay event-free.
nest-cli migration runm runRewrite ModifyBody / User / Public / setCreatedBy imports from @nest-extended/core to @nest-extended/decorators. Needed only when coming from the 0.0.x betas.
nest-cli versionvConfirm which CLI version is on your PATH.

From the 0.0.x betas

1.0.0 moved ModifyBody, User, Public and setCreatedBy out of @nest-extended/core into @nest-extended/decorators. One codemod rewrites those imports across src/**/*.ts:

Terminal
$nest-cli m run
Before — 0.0.x
import {
  NestController,
  ModifyBody,
  setCreatedBy,
} from '@nest-extended/core';
After — 1.0.0
import { NestController } from '@nest-extended/core';
import {
  ModifyBody,
  setCreatedBy,
} from '@nest-extended/decorators';

Then continue with the six steps above to reach v1.5.0.

Troubleshooting

Three things have to be true. One: NestExtendedModule.forRoot() is in app.module.ts — it is what discovers @ServiceEvents() classes. Two:both the service and its events class are in the feature module’s providers. Three: the caller uses create(), not _create(). The boot log confirms each wiring:
[NestExtendedEvents] CompanyEvents -> CompanyService

on* hooks are detached — they run after the response, on setImmediate. Await service.whenSettled() before asserting:
await service.create({ name: 'Acme' });
await service.whenSettled();
expect(mailer.send).toHaveBeenCalled();

Request-scoped services and events classes are not supported — the registry warns and skips them. Make both classes default-scoped, and read the request user from ctx.user instead of injecting REQUEST.

That is intended. remove() calls _patch internally to set the soft-delete flags, and _patch is on the event-free path — so a soft delete raises exactly one event.

The prisma CLI ships an 8.0.0 release candidate on the latest npm tag, so an unpinned install lands a pre-release next to a 7.x @prisma/client. Pin all three to ^7 — see step 3.

Leave the controllers calling the underscore methods and nothing changes. To switch the machinery off explicitly, pass { events: false } to a service’s super(...), or NestExtendedModule.forRoot({ events: false }) to disable service events and the controller hooks app-wide.
Upgraded?

Service events are the reason to be on 1.5.0 — the guide covers every hook, the context object, custom events, broadcasting and the limitations.