Custom Server
Next.js and NestJS share one HTTP server on a single port
Replace the default Next.js dev server with a custom Node.js entry point that boots both Next.js and NestJS in the same process. Requests to /api/* are dispatched to NestJS; everything else goes to Next.js. One process, one port, no proxy.
Setup
Give apps/api a workspace package name so it can be imported from apps/web:
{
"name": "@my-app/api",
"exports": {
".": "./src/index.ts"
}
}export { AppModule } from './app.module';Add the dependency and tsx to apps/web:
{
"dependencies": {
"@my-app/api": "workspace:*"
},
"devDependencies": {
"tsx": "^4.19.4"
},
"scripts": {
"dev": "tsx watch --tsconfig tsconfig.server.json server.ts"
}
}server.ts
import 'reflect-metadata';
import dotenv from 'dotenv';
import { resolve } from 'path';
dotenv.config({ path: resolve(process.cwd(), '../../.env') });
dotenv.config({ path: resolve(process.cwd(), '.env') });
import { NestFactory } from '@nestjs/core';
import { AppModule } from '@my-app/api';
import next from 'next';
import { createServer } from 'node:http';
const dev = process.env.NODE_ENV !== 'production';
const port = parseInt(process.env.PORT ?? '3000', 10);
async function main() {
const nextApp = next({ dev, port });
const handle = nextApp.getRequestHandler();
await nextApp.prepare();
const nestApp = await NestFactory.create(AppModule);
await nestApp.init();
const nestListener = nestApp
.getHttpServer()
.listeners('request')[0] as (req: any, res: any) => void;
const server = createServer((req, res) => {
if (req.url?.startsWith('/api/')) {
nestListener(req, res);
} else {
handle(req, res);
}
});
server.listen(port, () => {
console.log(`> Server ready on http://localhost:${port}`);
});
}
main().catch(console.error);tsconfig.server.json
NestJS decorators require emitDecoratorMetadata. Add a separate tsconfig for the server entry point so it doesn't affect the Next.js build:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "ES2023",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"strictNullChecks": true
},
"include": ["server.ts"],
"exclude": ["node_modules"]
}next.config.js
Remove the rewrite, since there is no separate API server to proxy to:
const nextConfig = {
output: "standalone",
};
export default nextConfig;Running
# Dev - tsx watches server.ts and restarts on changes
pnpm --filter=web dev
# Production
pnpm --filter=web build
node apps/web/server.jsTradeoffs
Single port: no reverse proxy, no rewrite config, no CORS concerns. The same port handles both the frontend and the API.
Custom server limitations: Next.js documents several features that don't work with a custom server, including automatic static optimization for some page types. For most apps this won't matter, but check the Next.js docs if you rely on specific rendering behaviors.
Startup: NestJS and Next.js both initialize before the server begins accepting requests, so there's no window where the API is unavailable during startup.