Platform Agnosticism & Standalone Apps
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against NestJS 12.1 (npm 12.1.2), TypeScript 6 (6.0.3), Node.js 20.19+ or 22.12+, Express 5 and Fastify 5, and was checked on 2026-10-02. The examples use a project generated by the Nest 12 CLI (ESM layout, Vitest, supertest); the nest-commander examples (nest-commander 3.21.0, whose peer range accepts @nestjs/common and @nestjs/core 12) live in a separate ESM workspace, and the REPL session shown is real output.
Nest is platform agnostic: a provider does not know whether it is called from an HTTP route, a message pattern, a WebSocket event, or a script that has no listener at all. The things that are tied to a transport (controllers, gateways, message handlers, pipes, guards, interceptors) are thin adapters around logic that lives in plain providers. This page shows the three consequences of that design. First, write the logic once and expose it over several transports (Writing logic once). Second, boot the dependency-injection (DI) container without any listener to build CLIs, cron workers and scripts (Standalone applications, Command-line applications with nest-commander). Third, open the container interactively with the REPL to inspect and call it (The REPL). The running Bookshelf scenario provides the modules: a BooksModule that exposes the catalogue, and a ReportsModule that builds a report from it. The module and provider concepts are explained in Modules and Providers & Dependency Injection.
HTTP, Express or Fastify"] M["BooksMessagesController
@MessagePattern, TCP"] W["BooksGateway
@SubscribeMessage, Socket.IO"] end subgraph Entry["Entry points without a listener"] S["createApplicationContext
cron job, script, worker"] C["nest-commander
CommandRunner"] R["repl(AppModule)
interactive"] end H --> B["BooksService
plain @Injectable"] M --> B W --> B S --> B C --> B R --> B
Writing logic once
The official page on platform agnosticism makes two claims: most components can be reused unchanged across different HTTP frameworks (Express and Fastify), and across different types of application: HTTP servers, microservices with different transports, and WebSockets. The mechanism is simply that a service is an ordinary @Injectable() class and every transport reaches it through DI.
import { Injectable } from '@nestjs/common';
export interface Book {
id: number;
title: string;
}
@Injectable()
export class BooksService {
private readonly books: Book[] = [
{ id: 1, title: 'Dune' },
{ id: 2, title: 'Emma' },
];
findAll(): Book[] {
return this.books;
}
findOne(id: number): Book | undefined {
return this.books.find((book) => book.id === id);
}
}
Source: NestJS: Platform agnosticism.
The service has no import from @nestjs/platform-express, @nestjs/microservices or @nestjs/websockets. Three adapters now expose the same findOne() over three transports. The HTTP controller (see Controllers & Routing):
import { Controller, Get, NotFoundException, Param, ParseIntPipe } from '@nestjs/common';
import { BooksService } from './books.service.js';
@Controller('books')
export class BooksController {
constructor(private readonly books: BooksService) {}
@Get()
findAll() {
return this.books.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
const book = this.books.findOne(id);
if (!book) throw new NotFoundException();
return book;
}
}
A controller that handles messages instead of HTTP requests, using @MessagePattern() from @nestjs/microservices (see Microservices Fundamentals):
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { BooksService } from './books.service.js';
@Controller()
export class BooksMessagesController {
constructor(private readonly books: BooksService) {}
@MessagePattern({ cmd: 'books.find-all' })
findAll() {
return this.books.findAll();
}
@MessagePattern({ cmd: 'books.find-one' })
findOne(@Payload() id: number) {
return this.books.findOne(id) ?? null;
}
}
Source: NestJS: Microservices overview.
And a WebSocket gateway (see WebSockets):
import { MessageBody, SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
import { BooksService } from './books.service.js';
@WebSocketGateway()
export class BooksGateway {
constructor(private readonly books: BooksService) {}
@SubscribeMessage('books.find-one')
findOne(@MessageBody() id: number) {
return this.books.findOne(id) ?? null;
}
}
Source: NestJS: Gateways.
All of them are declared in one module; the gateway is a provider, the two controllers are controllers, and the service is exported so that other modules (such as ReportsModule below) can use it:
import { Module } from '@nestjs/common';
import { BooksController } from './books.controller.js';
import { BooksGateway } from './books.gateway.js';
import { BooksMessagesController } from './books.microservice.js';
import { BooksService } from './books.service.js';
@Module({
controllers: [BooksController, BooksMessagesController],
providers: [BooksService, BooksGateway],
exports: [BooksService],
})
export class BooksModule {}
Source: NestJS: Modules.
To serve all three from one process, create the HTTP application and attach a microservice to it. This is a hybrid application: the HTTP server (with the WebSocket gateway, which by default shares its port) plus a TCP microservice listener. connectMicroservice() and startAllMicroservices() are the documented way to combine listeners:
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
options: { host: '127.0.0.1', port: 4010 },
});
await app.startAllMicroservices();
await app.listen(3010);
}
await bootstrap();
Source: NestJS: Hybrid application.
The check below starts that application and asks the same question over each transport: an HTTP GET /books/1, a TCP request through ClientProxyFactory, and a Socket.IO event with an acknowledgement. The test prints:
HTTP 200 {"id":1,"title":"Dune"}
TCP {"id":1,"title":"Dune"}
WS {"id":1,"title":"Dune"}
The test code is:
const client = ClientProxyFactory.create({
transport: Transport.TCP,
options: { host: '127.0.0.1', port: 4010 },
});
const tcp = await firstValueFrom(client.send({ cmd: 'books.find-one' }, 1));
const socket = io('http://127.0.0.1:3010'); // from socket.io-client
const ws = await new Promise((resolve) => socket.emit('books.find-one', 1, resolve));
Source: NestJS: Microservices overview.
Two rules keep this reuse honest.
-
Keep transport types out of services. A service that takes
Request,Responseor a Socket.IOSocketis no longer reusable. Controllers, handlers and gateways translate between the transport and plain arguments (@Param(),@Payload(),@MessageBody()), which is what they exist for. -
Remember what is not shared. Pipes, guards, interceptors and exception filters are bound per transport. A connected microservice does not inherit the global enhancers of the HTTP application unless you pass
inheritAppConfig: trueas the second argument ofconnectMicroservice(), and in that case theuseGlobal*()calls must come beforeconnectMicroservice(). The order ofstartAllMicroservices()andlisten()also matters: withstartAllMicroservices()first, message handlers can receive messages before the lifecycle hooks of every module have finished, so calllisten()(orinit()when there is no HTTP server) first if that matters. Authentication and validation written as HTTP guards and pipes therefore need a deliberate equivalent on the other transports; see Request Lifecycle.
The same idea extends to GraphQL: the dedicated GraphQL module is another entry point over the same providers, usable interchangeably with REST.
Standalone applications
A standalone application is the Nest IoC container without any network listener. NestFactory.createApplicationContext() returns an INestApplicationContext: it instantiates the modules and providers, runs the initialization lifecycle hooks, and hands you the container. It is the right tool for cron jobs started by the operating system or by Kubernetes, one-off scripts and database tasks, queue workers that do not need HTTP, and command-line tools. The official page lists exactly these uses.
Add a ReportsModule that depends on the exported BooksService and has a shutdown hook:
import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common';
import { BooksService } from '../books/books.service.js';
@Injectable()
export class ReportsService implements OnApplicationShutdown {
private readonly logger = new Logger(ReportsService.name);
constructor(private readonly books: BooksService) {}
catalogueReport(): string {
return this.books
.findAll()
.map((book) => `${book.id};${book.title}`)
.join('\n');
}
onApplicationShutdown(signal?: string) {
this.logger.log(`shutting down (signal: ${signal ?? 'none'})`);
}
}
import { Module } from '@nestjs/common';
import { BooksModule } from '../books/books.module.js';
import { ReportsService } from './reports.service.js';
@Module({
imports: [BooksModule],
providers: [ReportsService],
})
export class ReportsModule {}
Source: NestJS: Standalone applications.
The script that uses it is a separate entry file, next to main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { ReportsService } from './reports/reports.service.js';
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
try {
const reports = app.get(ReportsService);
console.log(reports.catalogueReport());
} finally {
await app.close();
}
}
await bootstrap();
Source: NestJS: Standalone applications.
Built with nest build and run with node dist/report.js, it prints the report, then the shutdown hook, and exits on its own; no port is opened. The Nest bootstrap logs, which createApplicationContext() prints by default, are omitted here:
1;Dune
2;Emma
LOG [ReportsService] shutting down (signal: none)
Three behaviours of the container matter for scripts.
get() and select()
app.get() is a query: it looks for the token (a class, a string, a symbol) in every module of the application and returns the instance, throwing if it does not exist. Without further options, the root module is selected, so app.get(ReportsService) works even though the provider lives in ReportsModule. Pass { strict: true } to require that the provider belongs to the selected module, and use select() to move through the module graph:
const books = app.select(BooksModule).get(BooksService, { strict: true });
// BooksService is not declared in the root module, so a strict lookup there fails:
app.get(BooksService, { strict: true });
// Nest could not find BooksService element (this provider does not exist in the current context)
Source: NestJS: Standalone applications.
In short: the strict lookup through select(BooksModule) returns the same singleton as the non-strict app.get(BooksService), and the strict lookup from the root module throws the message shown above. In non-strict mode get() is convenient for scripts; strict mode is useful when two modules declare providers with the same token and you must pick one.
| Method | Purpose |
|---|---|
|
Retrieves an instance of a controller or provider (including guards, filters and so on) available in the application context. |
|
Navigates the module graph to the given module and returns a context for it; chain it before |
Source: NestJS: Standalone applications.
Dynamic modules
For a dynamic module, select() needs the very object the module was registered with, so export it from a constant instead of calling register() twice:
import { DynamicModule, Inject, Injectable, Module } from '@nestjs/common';
export const LABEL_OPTIONS = Symbol('LABEL_OPTIONS');
@Injectable()
export class LabelService {
constructor(@Inject(LABEL_OPTIONS) private readonly options: { prefix: string }) {}
label(text: string): string {
return `${this.options.prefix}${text}`;
}
}
@Module({})
export class LabelModule {
static register(options: { prefix: string }): DynamicModule {
return {
module: LabelModule,
providers: [{ provide: LABEL_OPTIONS, useValue: options }, LabelService],
exports: [LabelService],
};
}
}
export const labelModule = LabelModule.register({ prefix: 'Bookshelf: ' });
@Module({ imports: [labelModule] })
export class LabelledAppModule {}
const app = await NestFactory.createApplicationContext(LabelledAppModule);
const label = app.select(labelModule).get(LabelService, { strict: true });
console.log(label.label('hello')); // Bookshelf: hello
Source: NestJS: Standalone applications.
Calling LabelModule.register({ prefix: 'x' }) again creates a different module definition; app.select() with that fresh object throws instead of finding the module. Dynamic modules are explained in Dynamic Modules.
close() and the shutdown phase
A standalone context holds resources (database pools, queue connections, timers) and the process stays alive while they are open. Call app.close() when the script is done, ideally in a finally block as above. close() triggers the shutdown lifecycle hooks (onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown(), in the order described in Lifecycle Events & Graceful Shutdown), which is where those resources are released; the ReportsService log line above is that hook running. A long-running worker does the opposite: it keeps the context open and calls app.enableShutdownHooks() so that SIGTERM closes it.
const app = await NestFactory.createApplicationContext(AppModule);
app.enableShutdownHooks();
// start consuming jobs here; SIGTERM/SIGINT now run the shutdown hooks and close the context
Source: NestJS: Lifecycle events.
What does not run
A standalone application has no listener, so everything tied to request handling is absent: middleware, guards, interceptors, pipes and exception filters. This is easy to forget, because app.get() returns the very same controller class that serves HTTP. The test below registers a global APP_INTERCEPTOR, calls the controller method directly in a standalone context, and then makes a real HTTP request to the same module:
@Module({
imports: [BooksModule],
providers: [{ provide: APP_INTERCEPTOR, useClass: LogInterceptor }],
})
class WithInterceptorModule {}
const ctx = await NestFactory.createApplicationContext(WithInterceptorModule);
ctx.get(BooksController).findAll(); // interceptor calls so far: 0
const http = await NestFactory.create(WithInterceptorModule);
await http.init();
await request(http.getHttpServer()).get('/books'); // interceptor calls: 1
Source: NestJS: Standalone applications.
The same applies to validation pipes and authorization guards: a script that calls a controller method skips them. Call services, not controllers, from standalone code, and do the validation the script needs explicitly. If you need the enhancers, use NestFactory.create() (and init() rather than listen() if you have no port to open). The serverless page of the official documentation gives the same advice for functions that need the full pipeline.
Command-line applications with nest-commander
You can write a CLI directly on createApplicationContext(), parsing process.argv yourself. nest-commander does that parsing for you and keeps the Nest structure: commands are injectable classes, options are decorated methods, and the container is created and closed by the library. It is built on the Commander library, and it is a third-party package maintained by the community, not by the NestJS core team: report problems in its own repository. It is documented in the official recipes and on its own site.
npm i nest-commander
Source: NestJS: Nest Commander.
A command extends the abstract CommandRunner class, is decorated with @Command(), and implements run(passedParams, options). Its first argument holds the positional parameters that did not match an option; the second is an object whose keys come from the option names. Each @Option() decorates a method that parses the raw string value:
import { Command, CommandRunner, Option } from 'nest-commander';
import { BooksService } from './books.service.js';
interface ReportOptions {
format?: 'csv' | 'json';
limit?: number;
}
@Command({ name: 'report', description: 'Prints the book catalogue' })
export class ReportCommand extends CommandRunner {
constructor(private readonly books: BooksService) {
super();
}
async run(_params: string[], options?: ReportOptions): Promise<void> {
const books = this.books.findAll().slice(0, options?.limit);
if (options?.format === 'json') {
console.log(JSON.stringify(books));
} else {
console.log(books.map((b) => `${b.id};${b.title}`).join('\n'));
}
}
@Option({ flags: '-f, --format [format]', description: 'csv (default) or json' })
parseFormat(value: string): 'csv' | 'json' {
if (value !== 'csv' && value !== 'json') {
throw new Error(`unsupported format: ${value}`);
}
return value;
}
@Option({ flags: '-l, --limit [limit]', description: 'maximum number of books' })
parseLimit(value: string): number {
return Number(value);
}
}
Source: NestJS: Nest Commander.
The command is an ordinary provider (Nest treats every command as @Injectable()), so it receives BooksService through its constructor and is registered in a module:
import { Module } from '@nestjs/common';
import { BooksService } from './books.service.js';
import { ReportCommand } from './report.command.js';
@Module({ providers: [BooksService, ReportCommand] })
export class AppModule {}
The entry point calls CommandFactory.run() with the root module. Nest’s logger is disabled by default under CommandFactory; the optional second argument is a logger or an array of log levels to keep, here ['warn', 'error']. Internally the factory calls NestFactory and app.close() when the command finishes, so there is no shutdown code to write:
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module.js';
async function bootstrap() {
await CommandFactory.run(AppModule, ['warn', 'error']);
}
await bootstrap();
Source: NestJS: Nest Commander.
After compiling with tsc and running the entry file, the commands behaved as follows:
node dist/main.js report
1;Dune
2;Emma
node dist/main.js report --format json --limit 1
[{"id":1,"title":"Dune"}]
node dist/main.js report --help
Usage: main report [options]
Prints the book catalogue
Options:
-f, --format [format] csv (default) or json
-l, --limit [limit] maximum number of books
-h, --help display help for command
Two pitfalls. An error thrown from an option parser (--format xml) is printed as Error: unsupported format: xml, but the process exit code is still 0 with nest-commander 3.21.0, so a script that calls the CLI should check the output or exit explicitly rather than rely on the exit code. And the file with CommandFactory.run() must be the process entry point, because the command line is read from process.argv when the factory runs.
For tests, nest-commander-testing provides CommandTestFactory, which builds the module like Test.createTestingModule() from @nestjs/testing, so overrideProvider() works before compile():
import { CommandTestFactory } from 'nest-commander-testing';
const moduleRef = await CommandTestFactory.createTestingCommand({ imports: [AppModule] })
.overrideProvider(BooksService)
.useValue({ findAll: () => [{ id: 9, title: 'Stub' }] })
.compile();
await CommandTestFactory.run(moduleRef, ['report', '--format', 'json']);
// console.log received: [{"id":9,"title":"Stub"}]
Source: NestJS: Nest Commander.
nest-commander also supports subcommands (subCommands on @Command() and CommandRunner.registerWithSubCommands()), aliases, interactive questions through Inquirer, and shell completion; its own documentation covers them. For a trivial script, createApplicationContext() plus a few lines of argument handling is lighter; reach for nest-commander once the tool has several commands or options.
The REPL
A REPL (read-eval-print loop) lets you load the whole dependency graph in a terminal, resolve providers, and call their methods while you type. It is built into @nestjs/core, so there is nothing to install. It is useful for exploring an unfamiliar codebase, trying a service against a development database, and debugging DI wiring (see also Debugging & Devtools).
Add a repl.ts file next to main.ts:
import { repl } from '@nestjs/core';
import { AppModule } from './app.module.js';
async function bootstrap() {
await repl(AppModule);
}
await bootstrap();
Source: NestJS: REPL.
Start it with the CLI, pointing --entryFile at the new file:
npm run start -- --entryFile repl
repl() returns a promise for a Node.js REPL server object, and the second argument takes the standard Node.js REPL options. Nest builds the container and then prints REPL initialized. The entry file in the session below is standalone/repl because the example lives in a subfolder; driving the compiled file through a pipe gives the same output as a terminal. The classes of all providers and controllers are available as globals, so you refer to them by name:
LOG [NestFactory] Starting Nest application...
LOG [InstanceLoader] AppModule dependencies initialized
LOG [InstanceLoader] ReportsModule dependencies initialized
LOG [InstanceLoader] BooksModule dependencies initialized
LOG REPL initialized
> get(ReportsService).catalogueReport()
'1;Dune\n2;Emma'
> methods(BooksService)
Methods:
◻ findAll
◻ findOne
> debug()
AppModule:
BooksModule:
- controllers:
◻ BooksController
◻ BooksMessagesController
- providers:
◻ BooksService
◻ BooksGateway
ReportsModule:
- providers:
◻ ReportsService
> appController = get(BooksController)
BooksController {
books: BooksService { books: [ [Object], [Object] ] }
}
> appController.findAll()
[ { id: 1, title: 'Dune' }, { id: 2, title: 'Emma' } ]
> select(BooksModule).get(BooksService).findOne(2)
{ id: 2, title: 'Emma' }
Source: NestJS: REPL.
You can assign results to variables, use await for asynchronous methods, and run any JavaScript. As in a standalone application, controllers called from the REPL do not go through guards, pipes or interceptors. Because the REPL boots the real modules, it connects to whatever your configuration points at: do not run it against a production database with write access.
Native functions
help() lists the built-in functions, and <function>.help (for example get.help) prints the signature of one. They are:
| Function | Description | Signature |
|---|---|---|
|
Retrieves an instance of a provider or controller; throws if it cannot be found. |
|
|
Resolves a transient or request-scoped provider or controller; throws if it cannot be found. |
|
|
Navigates the module tree, for example to pull out an instance from a specific module. |
|
|
Prints all registered modules with their controllers and providers; |
|
|
Prints the public methods of a provider or controller. |
|
|
Lists the native functions. |
|
Source: NestJS: REPL.
Request-scoped and transient providers (see Providers & Dependency Injection) cannot be returned by get(); use resolve(token, contextId) for them.
Watch mode and history
During development, run the REPL in watch mode so that code changes restart it:
npm run start -- --watch --entryFile repl
Each reload discards the command history. To keep it, call setupHistory() on the server object returned by repl(). The repl.ts used for the session above is exactly this:
import { repl } from '@nestjs/core';
import { AppModule } from './app.module.js';
async function bootstrap() {
const replServer = await repl(AppModule);
replServer.setupHistory('.nestjs_repl_history', (err) => {
if (err) {
console.error(err);
}
});
}
await bootstrap();
Source: NestJS: REPL.
In watch mode, nest start --watch --entryFile standalone/repl compiles the project, starts the REPL, answers get(BooksService).findOne(1) with { id: 1, title: 'Dune' }, and then recompiles and restarts the REPL (printing the ReportsService shutdown line) each time a file under the project changes. The history file is only written by an interactive terminal session; it is not created when input is piped. Add .nestjs_repl_history to .gitignore.
Choosing between the entry points
| You need | Use | Notes |
|---|---|---|
A server that answers requests |
|
Enhancers (guards, pipes, interceptors, filters) run for handlers. |
A one-off script or cron job |
|
No listener; enhancers do not run; close the context so the process can exit. |
A long-running worker without HTTP |
|
Keep the context open; see Events, Scheduling & Queues for cron jobs and queue consumers. |
A multi-command CLI |
|
Third-party; parsing, help and shutdown are handled for you. |
Interactive exploration |
|
Built in; development use; mind the data source it connects to. |
Functions on a serverless platform |
|
See the official serverless page; cold-start trade-offs are covered in Deployment. |
What changed recently, and where the book is outdated
-
Nest 12 ESM packages. The framework packages are ESM-only in Nest 12 and the CLI template is ESM, so the entry files above use
.jsimport extensions and top-levelawait bootstrap(), as the current REPL and Commander pages do. A CommonJS project drops the extensions and keeps the same API. -
Lifecycle hooks follow the hierarchy level in Nest 12. The order of
onModuleDestroy(),beforeApplicationShutdown()andonApplicationShutdown()triggered byapp.close()can differ from Nest 11; check scripts whose cleanup order matters. -
The documentation address changed. The standalone applications page was formerly called "application context"; the old
application-contextpath redirects tostandalone-applications. The behaviour is the same:NestFactory.createApplicationContext()still skips enhancers. -
The REPL is newer than the book. The built-in REPL, with
get(),select(),resolve(),debug(),methods(), watch mode and history, is among the features the book does not cover; use the official recipe and this page. -
Where the book is outdated. The book consulted for this section predates the Nest 12 ESM template, so code that uses extension-less imports and
requiredoes not compile unchanged in a project generated by the Nest 12 CLI. Use the code on this page. The bibliography is on the section index.