On this page
NestJS Integration with Analog
NestJS can serve your Analog app as a custom server. NestJS owns the process and its controllers, and Analog handles server-side rendering, page endpoints and server functions inside it. Server functions, page loads and server-side rendering can use your NestJS services directly.
Creating a new project
create-analog can scaffold a new project with this setup:
npm create analog@latest my-app -- --template latest --server nestjs
To add NestJS to an existing project, follow the steps below.
Step 1: Install NestJS
npm install @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata h3 srvxyarn add @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata h3 srvxpnpm add @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata h3 srvxNestJS's dependency injection reads decorator metadata, so enable it in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Vite 8 emits the metadata out of the box. On Vite 6 and 7, compile the server directory with SWC, for example through unplugin-swc, since esbuild can't emit decorator metadata.
Step 2: Configure Vite
Point the server option at the NestJS entry, and leave nitro() out of the plugins:
// vite.config.ts
import analog from '@analogjs/platform';
import angular from '@analogjs/vite-plugin-angular';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [analog({ server: { entry: 'src/server/main.ts' } }), angular()],
});
Step 3: Mount Analog in your module
Add Analog's handler in onModuleInit. NestJS calls it after registering every controller route and before installing its 404 handler, so your controllers take precedence and Analog only sees the requests they don't claim:
// src/server/app.module.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { createAnalogHandler } from '@analogjs/router/server';
import { GreetingService } from './greeting.service';
import { HelloController } from './hello.controller';
@Module({
controllers: [HelloController],
providers: [GreetingService],
})
export class AppModule implements OnModuleInit {
constructor(private readonly adapterHost: HttpAdapterHost) {}
onModuleInit(): void {
this.adapterHost.httpAdapter.use(createAnalogHandler());
}
}
Server-side rendering and page endpoints fetch Analog's own routes in-process. To reach your controllers from a page load or an HttpClient call during server-side rendering, pass a fetch to createAnalogHandler(), as described in server-side fetches, or use your NestJS services directly, as shown below.
Step 4: Create the server entry
The entry is a Fetchable module. srvx's toFetchHandler() turns NestJS's Express instance into the fetch the dev server calls, and the entry listens on its own port in production:
// src/server/main.ts
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { fileURLToPath } from 'node:url';
import { toFetchHandler } from 'srvx/node';
import { AppModule } from './app.module';
// Keeps the request body for Analog after Nest's body parsers have run.
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: true,
});
// Vite serves client files in development.
if (!import.meta.env.DEV) {
const clientDir = fileURLToPath(new URL('../client', import.meta.url));
app.useStaticAssets(clientDir, { index: false });
}
await app.init();
const server = app.getHttpAdapter().getInstance();
if (!import.meta.env.DEV) {
await app.listen(process.env['PORT'] ?? 3000);
}
export default {
fetch: toFetchHandler((req, res) => server(req, res)),
close: () => app.close(),
};
A few details matter here:
rawBody: truekeeps the original request bytes, so form actions and server functions can read bodies that NestJS's parsers already consumed.- Static files are registered before
app.init(). Anything registered after it sits behind NestJS's 404 handler, andinit()is also where the Analog handler is mounted. index: falsekeeps/server-rendered instead of returning the client'sindex.html.(req, res) => server(req, res)calls Express without anextcallback, so a request nothing handles gets a404instead of an empty response.- In development, Analog reloads the entry when server code changes, and calls
close()on the previous one.
Using the Fastify adapter
With @nestjs/platform-fastify, NestJS runs Analog's handler through @fastify/middie, which the adapter registers itself. Install the adapter and serve-static:
npm install @nestjs/platform-fastify serve-staticyarn add @nestjs/platform-fastify serve-staticpnpm add @nestjs/platform-fastify serve-staticThe module from Step 3 stays the same. The entry creates a Fastify app and exports Fastify's request handler as its fetch:
// src/server/main.ts
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
type NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { fileURLToPath } from 'node:url';
import serveStatic from 'serve-static';
import { toFetchHandler } from 'srvx/node';
import { AppModule } from './app.module';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
// Vite serves client files in development.
if (!import.meta.env.DEV) {
const clientDir = fileURLToPath(new URL('../client', import.meta.url));
app.use(serveStatic(clientDir, { index: false }));
}
await app.init();
const fastify = app.getHttpAdapter().getInstance();
await fastify.ready();
if (!import.meta.env.DEV) {
await app.listen(process.env['PORT'] ?? 3000, '0.0.0.0');
}
export default {
fetch: toFetchHandler((req, res) => fastify.routing(req, res)),
close: () => app.close(),
};
How it differs from Express:
- Middleware runs before Fastify's routes, so Analog sees requests before your controllers. Your controllers still answer their routes under
/api/and their non-GETroutes, because Analog passes those on, but aGETcontroller route outside/api/would be server-rendered. Keep controllers under/api/, for example withapp.setGlobalPrefix('api'). - Analog reads request bodies before Fastify parses them, so
rawBodyisn't needed, and malformed JSON sent to a server function gets Analog's400. - Static files go through
serve-staticas middleware, registered beforeinit().useStaticAssets()registers a Fastify route, which Analog's handler would run ahead of. listen()binds tolocalhostunless given a host;'0.0.0.0'accepts outside connections, for example in a container.
Development
Under vite, NestJS runs inside the dev server rather than listening on its own port:
- The first request Vite doesn't serve itself loads the entry, which runs
NestFactory.create()andinit(). - Vite serves client files and HMR, and passes every other request to NestJS, which routes it as in production.
- Vite compiles the NestJS code, so there's no
nest start --watchortscstep. - When server code changes, the next request reloads the entry, creating a new NestJS app, and calls
close()on the previous one, so shutdown hooks run. - To reach controllers from server-side fetches, point the origin at Vite's dev server, such as
http://localhost:5173.
Using NestJS services from Angular code
Server functions, page loads and server-side rendering run inside the NestJS process, so they can use the same service instances as your controllers. Expose NestJS's ModuleRef through an Angular injection token:
// src/server/inject-nest.ts
import { InjectionToken, inject } from '@angular/core';
import type { Type } from '@nestjs/common';
import type { ModuleRef } from '@nestjs/core';
let moduleRef: ModuleRef | undefined;
export function setNestApp(ref: ModuleRef): void {
moduleRef = ref;
}
export const NEST_APP = new InjectionToken<ModuleRef>('NEST_APP', {
providedIn: 'root',
factory: () => {
if (!moduleRef) {
throw new Error('The NestJS application is not initialized.');
}
return moduleRef;
},
});
export function injectNest<T>(token: Type<T> | string | symbol): T {
return inject(NEST_APP).get(token, { strict: false });
}
Set it from your module:
// src/server/app.module.ts
import { HttpAdapterHost, ModuleRef } from '@nestjs/core';
import { setNestApp } from './inject-nest';
export class AppModule implements OnModuleInit {
constructor(
private readonly adapterHost: HttpAdapterHost,
moduleRef: ModuleRef,
) {
setNestApp(moduleRef);
}
// ...
}
Then resolve NestJS providers in a server function:
// src/app/greeting.server.ts
import { serverFn } from '@analogjs/router/server';
import * as v from 'valibot';
import { injectNest } from '../server/inject-nest';
import { GreetingService } from '../server/greeting.service';
export const greet = serverFn(
v.object({ name: v.string() }),
async ({ name }) => injectNest(GreetingService).greet(name),
);
.server.ts files are server-only, so the client bundle gets a small proxy and none of the NestJS code.
Building and running
npm run build
node dist/server/main.js
Limitations
- With the Express adapter, malformed JSON sent to a server function gets NestJS's
400response rather than Analog's, because NestJS's JSON parser runs first. - WebSockets go through NestJS gateways.