@nestjs/core
Version:
Nest - modern, fast, powerful node.js web framework (@core)
459 lines (458 loc) • 19.9 kB
JavaScript
import { VersioningType, } from '@nestjs/common';
import { iterate } from 'iterare';
import { platform } from 'os';
import { ApplicationConfig } from './application-config.js';
import { MESSAGES } from './constants.js';
import { optionalRequire } from './helpers/optional-require.js';
import { makeSafeInstanceDecorator } from './helpers/safe-instance-decorator.js';
import { Injector } from './injector/injector.js';
import { MiddlewareContainer } from './middleware/container.js';
import { MiddlewareModule } from './middleware/middleware-module.js';
import { mapToExcludeRoute } from './middleware/utils.js';
import { NestApplicationContext } from './nest-application-context.js';
import { RoutesResolver } from './router/routes-resolver.js';
import { Logger } from '@nestjs/common';
import { loadPackage, loadPackageCached, tryLoadPackage, addLeadingSlash, isFunction, isObject, isString, } from '@nestjs/common/internal';
import { RouteConflictDetector } from './router/route-conflict-detector.js';
import { RouteSpecificitySorter } from './router/route-specificity-sorter.js';
/**
* @publicApi
*/
export class NestApplication extends NestApplicationContext {
httpAdapter;
config;
graphInspector;
logger = new Logger(NestApplication.name, {
timestamp: true,
});
middlewareModule;
middlewareContainer = new MiddlewareContainer(this.container);
microservicesModule = null;
socketModule = null;
routesResolver;
microservices = [];
httpServer;
isListening = false;
isWsModuleRegistered = false;
constructor(container, httpAdapter, config, graphInspector, appOptions = {}) {
super(container, appOptions);
this.httpAdapter = httpAdapter;
this.config = config;
this.graphInspector = graphInspector;
this.config.setRouteConflictPolicy(appOptions.routeConflictPolicy);
this.config.setRouteResolutionStrategy(appOptions.routeResolutionStrategy);
this.selectContextModule();
this.registerHttpServer();
this.injector = new Injector({
preview: this.appOptions.preview,
instanceDecorator: appOptions.instrument?.instanceDecorator,
});
this.middlewareModule = new MiddlewareModule();
this.routesResolver = new RoutesResolver(this.container, this.config, this.injector, this.graphInspector);
}
async prepareClose() {
this.httpAdapter && (await this.httpAdapter.beforeClose?.());
}
async dispose() {
await this.socketModule?.close();
await this.microservicesModule?.close();
await this.httpAdapter?.close();
await Promise.all(iterate(this.microservices).map(async (microservice) => {
microservice.setIsTerminated(true);
await microservice.close();
}));
}
getHttpAdapter() {
return this.httpAdapter;
}
registerHttpServer() {
this.httpServer = this.createServer();
}
getUnderlyingHttpServer() {
return this.httpAdapter.getHttpServer();
}
applyOptions() {
if (!this.appOptions || !this.appOptions.cors) {
return undefined;
}
const passCustomOptions = isObject(this.appOptions.cors) || isFunction(this.appOptions.cors);
if (!passCustomOptions) {
return this.enableCors();
}
return this.enableCors(this.appOptions.cors);
}
createServer() {
this.httpAdapter.initHttpServer(this.appOptions);
return this.httpAdapter.getHttpServer();
}
async registerModules() {
await this.registerWsModule();
if (this.microservicesModule) {
this.microservicesModule.register(this.container, this.graphInspector, this.config, this.appOptions);
this.microservicesModule.setupClients(this.container);
}
await this.middlewareModule.register(this.middlewareContainer, this.container, this.config, this.injector, this.httpAdapter, this.graphInspector, this.appOptions);
}
async registerWsModule() {
if (!this.socketModule) {
return;
}
await this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
this.isWsModuleRegistered = true;
}
async init() {
if (this.isInitialized) {
return this;
}
// Lazy-load optional modules (ESM-compatible)
await Promise.all([
this.loadSocketModule(),
this.loadMicroservicesModule(),
]);
this.applyOptions();
await this.httpAdapter?.init?.();
const useBodyParser = this.appOptions && this.appOptions.bodyParser !== false;
useBodyParser && this.registerParserMiddleware();
await this.registerModules();
await this.registerRouter();
await this.callInitHook();
await this.registerRouterHooks();
await this.callBootstrapHook();
this.isInitialized = true;
this.logger.log(MESSAGES.APPLICATION_READY);
return this;
}
registerParserMiddleware() {
const prefix = this.config.getGlobalPrefix();
const rawBody = !!this.appOptions?.rawBody;
this.httpAdapter.registerParserMiddleware(prefix, rawBody);
}
async registerRouter() {
await this.registerMiddleware(this.httpAdapter);
const prefix = this.config.getGlobalPrefix();
const basePath = addLeadingSlash(prefix);
const conflictPolicy = this.config.getRouteConflictPolicy();
const resolutionStrategy = this.config.getRouteResolutionStrategy();
const adapterIsOrderSensitive = this.httpAdapter.isRouteOrderSensitive?.() ?? true;
const shouldSortBySpecificity = resolutionStrategy === 'specificity' && adapterIsOrderSensitive;
// Adapters that are not order-sensitive (e.g. Fastify) currently
// also reject duplicate (method, URL) registrations synchronously
// from the underlying router. Treat the two properties as one
// signal until a separate capability flag is introduced.
const adapterRejectsDuplicates = !adapterIsOrderSensitive;
if (!conflictPolicy && !shouldSortBySpecificity) {
this.routesResolver.resolve(this.httpAdapter, basePath);
return;
}
// Defer registration whenever we collect routes for diagnostics or
// re-ordering. In particular, when a conflict policy is set we
// must run detection *before* the adapter sees any route, because
// duplicate-rejecting adapters like Fastify throw synchronously
// from `instance.route()` and would short-circuit both the resolve
// loop and the aggregated `RouteConflictException`.
const resolvedRoutes = [];
this.routesResolver.resolve(this.httpAdapter, basePath, {
onRouteResolved: route => resolvedRoutes.push(route),
deferRegistration: true,
});
// Sort before conflict detection so that winner/shadowed pairs in every
// conflict record reflect actual adapter registration order. Without this,
// the reported winner could be the declaration-first (less-specific) route
// even though the sorted-first (more-specific) route is what really wins.
const orderedRoutes = shouldSortBySpecificity
? RouteSpecificitySorter.sort(resolvedRoutes)
: resolvedRoutes;
const routesToSkip = new Set();
if (conflictPolicy) {
const filteredPolicy = adapterIsOrderSensitive
? conflictPolicy
: { duplicate: conflictPolicy.duplicate };
const conflicts = RouteConflictDetector.detect(orderedRoutes, this.config.getVersioning());
// On adapters that reject duplicate registrations the policy
// cannot be honoured by simply logging — the adapter would
// throw on the second `instance.route()` call. Drop the
// shadowed route of every duplicate conflict so the detector
// (not the adapter) decides which one wins. The detector
// always picks the earlier-registered route as the winner.
if (adapterRejectsDuplicates) {
conflicts.forEach(conflict => {
if (conflict.kind === 'duplicate') {
routesToSkip.add(conflict.shadowed);
}
});
}
// When specificity sorting is active, shadow conflicts where the sort
// promoted the winner (declared later but more specific) are resolved
// at runtime: the more-specific route is first-registered and handles
// its requests, the less-specific route handles the rest. Filtering
// these out prevents shadow: 'error' from aborting an app whose routes
// work correctly after specificity ordering. Genuine shadows — where the
// winner was already first in declaration order and the sort did not help
// — are kept and still apply the configured policy.
const effectiveConflicts = shouldSortBySpecificity
? RouteConflictDetector.filterSortResolvedShadows(conflicts, resolvedRoutes)
: conflicts;
RouteConflictDetector.handle(effectiveConflicts, filteredPolicy, this.logger);
}
orderedRoutes.forEach(route => {
if (routesToSkip.has(route))
return;
this.routesResolver.registerResolvedRoute(this.httpAdapter, route);
});
}
async registerRouterHooks() {
this.routesResolver.registerNotFoundHandler();
this.routesResolver.registerExceptionHandler();
}
connectMicroservice(microserviceOptions, hybridAppOptions = {}) {
const { NestMicroservice } = loadPackageCached('@nestjs/microservices', 'NestFactory');
const { inheritAppConfig } = hybridAppOptions;
const applicationConfig = inheritAppConfig
? this.config
: new ApplicationConfig();
const instance = new NestMicroservice(this.container, microserviceOptions, this.graphInspector, applicationConfig);
if (!hybridAppOptions.deferInitialization) {
instance.registerListeners();
instance.setIsInitialized(true);
instance.setIsInitHookCalled(true);
}
this.microservices.push(instance);
return instance;
}
getMicroservices() {
return this.microservices;
}
getHttpServer() {
return this.httpServer;
}
async startAllMicroservices() {
this.assertNotInPreviewMode('startAllMicroservices');
await Promise.all(this.microservices.map(msvc => msvc.listen()));
return this;
}
use(...args) {
this.httpAdapter.use(...this.applyFunctionDecoratorIfRegistered(args));
return this;
}
useBodyParser(...args) {
if (!('useBodyParser' in this.httpAdapter)) {
this.logger.warn('Your HTTP Adapter does not support `.useBodyParser`.');
return this;
}
const [parserType, ...otherArgs] = args;
const rawBody = !!this.appOptions.rawBody;
this.httpAdapter.useBodyParser?.(...[parserType, rawBody, ...otherArgs]);
return this;
}
enableCors(options) {
this.httpAdapter.enableCors(options);
}
enableVersioning(options = { type: VersioningType.URI }) {
this.config.enableVersioning(options);
return this;
}
async listen(port, ...args) {
this.assertNotInPreviewMode('listen');
if (!this.isInitialized) {
await this.init();
}
const httpAdapterHost = this.container.getHttpAdapterHostRef();
return new Promise((resolve, reject) => {
const errorHandler = (e) => {
this.logger.error(e?.toString?.());
reject(e);
};
this.httpServer.once('error', errorHandler);
const isCallbackInOriginalArgs = isFunction(args[args.length - 1]);
const listenFnArgs = isCallbackInOriginalArgs
? args.slice(0, args.length - 1)
: args;
this.httpAdapter.listen(port, ...listenFnArgs, (...originalCallbackArgs) => {
if (this.appOptions?.autoFlushLogs ?? true) {
this.flushLogs();
}
if (originalCallbackArgs[0] instanceof Error) {
return reject(originalCallbackArgs[0]);
}
const address = this.httpServer.address();
if (address) {
this.httpServer.removeListener('error', errorHandler);
this.isListening = true;
httpAdapterHost.listening = true;
resolve(this.httpServer);
}
if (isCallbackInOriginalArgs) {
args[args.length - 1](...originalCallbackArgs);
}
});
});
}
async getUrl() {
return new Promise((resolve, reject) => {
if (!this.isListening) {
this.logger.error(MESSAGES.CALL_LISTEN_FIRST);
reject(MESSAGES.CALL_LISTEN_FIRST);
return;
}
const address = this.httpServer.address();
resolve(this.formatAddress(address));
});
}
formatAddress(address) {
if (isString(address)) {
if (platform() === 'win32') {
return address;
}
const basePath = encodeURIComponent(address);
return `${this.getProtocol()}+unix://${basePath}`;
}
let host = this.host();
if (address && address.family === 'IPv6') {
if (host === '::') {
host = '[::1]';
}
else {
host = `[${host}]`;
}
}
else if (host === '0.0.0.0') {
host = '127.0.0.1';
}
return `${this.getProtocol()}://${host}:${address.port}`;
}
setGlobalPrefix(prefix, options) {
this.config.setGlobalPrefix(prefix);
if (options) {
const exclude = options?.exclude
? mapToExcludeRoute(options.exclude)
: [];
this.config.setGlobalPrefixOptions({
...options,
exclude,
});
}
return this;
}
useWebSocketAdapter(adapter) {
if (this.isWsModuleRegistered) {
this.logger.warn('useWebSocketAdapter() was called after WebSocket gateways were already initialized. The provided adapter will be stored but will NOT be applied to existing gateways — they remain bound to the previously installed adapter. To install a custom adapter, call app.useWebSocketAdapter(...) BEFORE app.init() (or app.listen()).');
}
this.config.setIoAdapter(adapter);
return this;
}
useGlobalFilters(...filters) {
filters = this.applyInstanceDecoratorIfRegistered(...filters);
this.config.useGlobalFilters(...filters);
filters.forEach(item => this.graphInspector.insertOrphanedEnhancer({
subtype: 'filter',
ref: item,
}));
return this;
}
useGlobalPipes(...pipes) {
pipes = this.applyInstanceDecoratorIfRegistered(...pipes);
this.config.useGlobalPipes(...pipes);
pipes.forEach(item => this.graphInspector.insertOrphanedEnhancer({
subtype: 'pipe',
ref: item,
}));
return this;
}
useGlobalInterceptors(...interceptors) {
interceptors = this.applyInstanceDecoratorIfRegistered(...interceptors);
this.config.useGlobalInterceptors(...interceptors);
interceptors.forEach(item => this.graphInspector.insertOrphanedEnhancer({
subtype: 'interceptor',
ref: item,
}));
return this;
}
useGlobalGuards(...guards) {
guards = this.applyInstanceDecoratorIfRegistered(...guards);
this.config.useGlobalGuards(...guards);
guards.forEach(item => this.graphInspector.insertOrphanedEnhancer({
subtype: 'guard',
ref: item,
}));
return this;
}
useStaticAssets(pathOrOptions, options) {
this.httpAdapter.useStaticAssets?.(pathOrOptions, options);
return this;
}
setBaseViewsDir(path) {
this.httpAdapter.setBaseViewsDir?.(path);
return this;
}
setViewEngine(engineOrOptions) {
this.httpAdapter.setViewEngine?.(engineOrOptions);
return this;
}
/**
* Pre-load optional packages so that createNestApplication,
* createNestMicroservice and createHttpAdapter can stay synchronous.
*/
async preloadLazyPackages() {
// Best-effort: silently swallow if packages are not installed
await tryLoadPackage('@nestjs/platform-express', () => import('@nestjs/platform-express'));
await tryLoadPackage('@nestjs/microservices', () => import('@nestjs/microservices'));
}
host() {
const address = this.httpServer.address();
if (isString(address)) {
return undefined;
}
return address && address.address;
}
getProtocol() {
return this.appOptions && this.appOptions.httpsOptions ? 'https' : 'http';
}
async registerMiddleware(instance) {
await this.middlewareModule.registerMiddleware(this.middlewareContainer, instance);
}
applyInstanceDecoratorIfRegistered(...instances) {
if (this.appOptions.instrument?.instanceDecorator) {
const decorate = makeSafeInstanceDecorator(this.appOptions.instrument.instanceDecorator);
return instances.map(instance => decorate(instance));
}
return instances;
}
applyFunctionDecoratorIfRegistered(args) {
if (!this.appOptions.instrument?.instanceDecorator) {
return args;
}
const decorate = makeSafeInstanceDecorator(this.appOptions.instrument.instanceDecorator);
// Decorators may return a non-function value for plain middleware
// functions; fall back to the original argument so the HTTP adapter
// always receives a valid handler.
const decorateFunction = (arg) => {
if (!isFunction(arg)) {
return arg;
}
const decorated = decorate(arg);
return isFunction(decorated) ? decorated : arg;
};
// Map over the original arguments to preserve arity: appending a trailing
// `undefined` to a single-argument `use(fn)` call would make Express 5's
// router throw "argument handler must be a function".
return args.map(decorateFunction);
}
async loadSocketModule() {
if (!this.socketModule) {
const socketModule = await optionalRequire('@nestjs/websockets/socket-module', () => import('@nestjs/websockets/socket-module.js'));
if (socketModule?.SocketModule) {
this.socketModule = new socketModule.SocketModule();
}
}
}
async loadMicroservicesModule() {
if (!this.microservicesModule) {
const msModule = await optionalRequire('@nestjs/microservices/microservices-module', () => import('@nestjs/microservices/microservices-module.js'));
if (msModule?.MicroservicesModule) {
this.microservicesModule = new msModule.MicroservicesModule();
// Pre-cache the main barrel so connectMicroservice() can stay synchronous
await loadPackage('@nestjs/microservices', 'NestFactory', () => import('@nestjs/microservices'));
}
}
}
}