NestJS vs Next.js

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.

NestJS and Next.js are different tools that share a similar name and a language. Both are written in TypeScript and run on Node.js, so they are easy to confuse. This page is written against NestJS 12.1 (Express 5 by default, Fastify 5 optional, TypeScript 6, Node.js 20.19+ or 22.12+) and Next.js 16.3 (React 19, Node.js 20.9+), with the versions checked on 2026-10-02. In one line: NestJS builds the backend (an API), Next.js builds the web front end (pages) that may call one.

The one-minute answer

  • NestJS is a framework for server-side Node.js applications. Its architecture is inspired by Angular (modules, controllers, providers, decorators, dependency injection) and it runs on top of an HTTP server library, Express by default or Fastify. It produces REST and GraphQL APIs, WebSocket gateways and microservices. It has no view layer of its own and never renders React components.

  • Next.js is Vercel’s React framework. It renders React components on the server (SSR), at build time (SSG) or as React Server Components, hydrates them in the browser, and provides file-based routing, route handlers and server actions so one project can hold both the pages and some backend code.

If the question is "which one is my backend?", the answer is NestJS. If it is "which one produces my React pages?", it is Next.js. The generic ideas behind Next.js rendering modes are explained on Server Rendering, SSG and React Server Components, and this site has a dedicated Next.js section.

Side by side

NestJS Next.js

Purpose

Server-side application framework: APIs, real-time gateways, background workers, microservices.

Full-stack React framework: pages, layouts and a thin backend layer for the same app.

Rendering

None. It returns JSON, text, streams or files. Template engines can be wired in, but UI is not its job.

SSR, SSG, incremental regeneration and React Server Components, plus client components that hydrate in the browser.

Routing

Explicit: @Controller('books') plus @Get(), @Post() and similar decorators on methods.

File-system based: folders under app/ map to URLs, page.tsx renders a page and route.ts is an HTTP endpoint.

Dependency injection

Built in and central: providers are registered in modules and injected through constructors.

None built in. Code imports modules directly; sharing is done with plain functions and modules.

Data layer

Bring your own, wired through providers: TypeORM, Prisma, Drizzle, Mongoose and others.

Bring your own, called directly from server components, route handlers and server actions, with caching options for fetch.

Runs where

Always on a Node.js server (or a serverless wrapper). Express or Fastify underneath.

A Node.js server, or static files for static export. Server components never run in the browser.

Typical deployment

A long-lived container or process behind a load balancer, scaled horizontally, often next to queues and databases.

A Node.js server or Docker container, a platform that supports Next.js, or a static export.

Cross-cutting features

Pipes, guards, interceptors, filters and middleware around every handler.

proxy.ts, layouts, server component boundaries and route-level conventions.

Because NestJS is organized around architecture and Next.js around the page, it is normal to see both in the same product: Next.js for what the browser shows, NestJS for the domain logic behind it. NestJS is also the framework used by the rest of this section; start with Introduction and architecture.

The two frameworks in code

The same "list books" feature looks very different. In NestJS a controller class declares the route and returns data.

A NestJS controller (src/books/books.controller.ts)
import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiCreatedResponse, ApiOkResponse, ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';

export class CreateBookDto {
  @ApiProperty()
  @IsString()
  @MinLength(1)
  title: string;
}

export class BookDto {
  @ApiProperty()
  id: number;

  @ApiProperty()
  title: string;
}

@Controller('books')
export class BooksController {
  private readonly books: BookDto[] = [{ id: 1, title: 'Dune' }];

  @Get()
  @ApiOkResponse({ type: [BookDto] })
  findAll(): BookDto[] {
    return this.books;
  }

  @Post()
  @ApiCreatedResponse({ type: BookDto })
  create(@Body() dto: CreateBookDto): BookDto {
    const book = { id: this.books.length + 1, title: dto.title };
    this.books.push(book);
    return book;
  }
}

CreateBookDto uses class-validator decorators, which ValidationPipe enforces (shown next), and @ApiProperty() exposes the shape to the OpenAPI document. A request without a title is rejected with 400 Bad Request before the handler runs.

In Next.js the folder structure is the router, and a page is a React component. A page is not part of the NestJS example above and cannot be compared line by line; the following server component shows how Next.js fetches data on the server and returns markup instead of JSON. See Next.js Routing Fundamentals for how folders map to routes.

A Next.js server component (app/books/page.tsx)
import { getBooks } from '@/lib/books-api';

export default async function BooksPage() {
  const books = await getBooks();
  return (
    <ul>
      {books.map((book) => (
        <li key={book.id}>{book.title}</li>
      ))}
    </ul>
  );
}

The component runs on the server, awaits the data and sends HTML. getBooks is defined in the next section.

How they are combined

The most common arrangement is a Next.js application that renders the UI and calls a separate NestJS API. When Next.js acts as the single entry point that the browser talks to and forwards or shapes calls to other services, it is playing the backend for frontend (BFF) role. Next.js documents this pattern in How to use Next.js as a backend for your frontend; see also Route Handlers and Backend for Frontend.

flowchart LR browser([Browser]) subgraph web["Next.js (React front end and BFF)"] pages["Server and client components"] bff["Route handlers and server actions"] end subgraph api["NestJS API"] ctrl["Controllers, pipes, guards"] svc["Providers (business logic)"] end db[("PostgreSQL")] browser -->|"HTML, RSC payload"| pages browser -->|"/api/books"| bff pages -->|"fetch on the server"| ctrl bff -->|"fetch on the server"| ctrl ctrl --> svc svc -->|"TypeORM, Prisma, Drizzle"| db

Three pieces make the combination work.

A typed API client in the Next.js project

Next.js server code reads the API address from an environment variable and keeps every call in one module, so pages and route handlers never hard-code a URL.

A data-access module in the Next.js project (lib/books-api.ts)
// lib/books-api.ts in the Next.js project: the only place that knows the API URL.
export interface Book {
  id: number;
  title: string;
}

const API_URL = process.env.API_URL ?? 'http://localhost:3000';

export async function getBooks(): Promise<Book[]> {
  const res = await fetch(`${API_URL}/books`);
  if (!res.ok) throw new Error(`Books API answered ${res.status}`);
  return (await res.json()) as Book[];
}

A server component, a route handler or a server action can all call getBooks(). Because it runs on the server, API_URL can be an internal network address that the browser never sees.

Route handlers and server actions as the BFF layer

A route handler turns the NestJS answer into whatever shape the UI needs, while a server action sends a form submission to the API (see Server Actions and Mutations).

A Next.js route handler as a thin BFF endpoint (app/api/books/route.ts)
import { getBooks } from '@/lib/books-api';

export async function GET(): Promise<Response> {
  const books = await getBooks();
  return Response.json(books.map(({ id, title }) => ({ id, title })));
}
A Next.js server action that forwards a form to NestJS (app/lib/actions.ts)
'use server';

const API_URL = process.env.API_URL ?? 'http://localhost:3000';

export async function addBook(formData: FormData): Promise<void> {
  const title = formData.get('title');
  if (typeof title !== 'string' || title.length === 0) {
    throw new Error('title is required');
  }
  const res = await fetch(`${API_URL}/books`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title }),
  });
  if (!res.ok) throw new Error(`Books API answered ${res.status}`);
}

Server actions can be invoked with a direct POST request and not just from the UI, so a real application checks authentication and authorization inside each one, as the Next.js documentation points out. The NestJS API must also enforce its own authorization; never rely on the Next.js layer alone, because anyone who can reach the NestJS port can bypass it.

Letting the browser call NestJS directly with CORS

When client components call the NestJS API from the browser instead of going through the BFF, the API must allow the Next.js origin. NestJS exposes this through enableCors(). The allowed origin is read from configuration, so each environment can set its own.

Configuring CORS, validation and the OpenAPI document in NestJS (src/configure.ts)
import { ValidationPipe, type INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

export function configure(app: INestApplication): void {
  const config = app.get(ConfigService);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
  // The Next.js origin(s) allowed to call this API from a browser.
  app.enableCors({
    origin: config.get<string>('WEB_ORIGIN', 'http://localhost:3001'),
  });
  const document = SwaggerModule.createDocument(
    app,
    new DocumentBuilder().setTitle('Bookshelf API').setVersion('1.0').build(),
  );
  // Serves Swagger UI at /api and the JSON document at /api-json.
  SwaggerModule.setup('api', app, document);
}
Bootstrapping the API (src/main.ts)
import { NestFactory } from '@nestjs/core';
import { BooksModule } from './books.module.js';
import { configure } from './configure.js';

const app = await NestFactory.create(BooksModule);
configure(app);
await app.listen(process.env.PORT ?? 3000);

Starting the API and requesting a book from an allowed origin shows the CORS header:

PORT=3000 node dist/main.js &
curl -i http://localhost:3000/books -H 'Origin: http://localhost:3001'

The response includes the allowed origin, followed by the JSON body:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3001
Content-Type: application/json; charset=utf-8

[{"id":1,"title":"Dune"}]

Sharing types through OpenAPI

Next.js and NestJS live in different packages (often different repositories), so the shared contract is the API’s description rather than a shared class. NestJS generates an OpenAPI document with @nestjs/swagger, served as JSON at /api-json in the configuration above; see OpenAPI and Swagger for the full feature set. The Next.js project then generates TypeScript types from that document with openapi-typescript:

npx openapi-typescript http://localhost:3000/api-json -o src/api.d.ts

The generated file contains a components interface with one entry per DTO that the document describes. A schema only appears when the controller declares it, which is why the controller above uses @ApiOkResponse({ type: [BookDto] }) and @ApiCreatedResponse({ type: BookDto }). Without them (or the Nest CLI plugin, described in the OpenAPI page) the document lists the request body but not the response shape, and the generated client has no Book type.

Using the generated types in the Next.js project (lib/typed-client.ts)
import type { components } from './api.js';

export type Book = components['schemas']['BookDto'];
export type CreateBook = components['schemas']['CreateBookDto'];

export async function createBook(apiUrl: string, body: CreateBook): Promise<Book> {
  const res = await fetch(`${apiUrl}/books`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  return (await res.json()) as Book;
}

When the API is a GraphQL API instead, the same idea applies with a schema-driven code generator; see GraphQL and NestJS GraphQL. In a single monorepo where both apps are TypeScript, the two can also import the same package of types, at the cost of coupling their release cycles.

When Next.js is enough and when a separate NestJS backend pays off

A Next.js project can already contain backend code: server components read data, route handlers expose HTTP endpoints and server actions handle mutations. The Next.js documentation itself says these capabilities are not a full backend replacement. They form a public, HTTP-based API layer.

Route handlers and server actions are enough when A separate NestJS backend pays off when

One web app consumes the data and no other client exists.

Web, mobile, partners and internal tools all consume the same API.

Logic is mostly "read from a database, render, save a form".

There is substantial domain logic that benefits from modules, dependency injection and unit-testable providers.

The team is a small full-stack React team.

Separate backend and front-end teams need an explicit contract (OpenAPI or GraphQL) and independent releases.

Request/response is the only interaction.

You need WebSocket gateways, microservice transports, queues, scheduled jobs or a standalone application with no HTTP at all.

Deployment is a single Next.js app.

The API must scale, fail over or be secured independently of the front end.

A pragmatic path is to start with route handlers and server actions, and move logic into NestJS once a second client, a worker or a team boundary appears. Because the Next.js code from the earlier sections only reads API_URL, switching the data source from "in process" to "NestJS over HTTP" touches lib/books-api.ts and not the pages.

Common confusions

  • "NextJS" and "NestJS" are not interchangeable in job titles or search results. The spelling NestJS has an s and means the Angular-style backend framework; Next.js or NextJS has an x and means the React framework.

  • NestJS cannot render a React page by itself. It can serve a built single-page app’s static files, for example with the @nestjs/serve-static package, but that is file hosting and not server-side rendering.

  • Next.js is not an API framework with controllers or dependency injection. Route handlers are plain functions over the Web Request and Response objects.

  • Running both is common, and they need different ports (for example Next.js on 3001 and NestJS on 3000 locally) which is exactly why CORS and an API_URL setting appear above.

  • Each has its own request pipeline. NestJS has middleware, guards, interceptors and pipes around controllers; Next.js has proxy.ts (see below, and Redirects, Rewrites and Proxy) plus layouts and route conventions.

What changed recently and where older material is outdated

  • NestJS 12 (released 2026-08-27) made its core packages ESM-only, and its generated project now defaults to ESM with Vitest. Older tutorials and the NestJS book predate this and show the CommonJS and Jest setup; both module systems remain available through the nest new prompt, and the examples above use the ESM form (.js import extensions, top-level await).

  • Older Next.js material is built on the pages/ router and getServerSideProps. Current Next.js documentation is written for the app/ router, where server components fetch data and route.ts replaces API routes, which the Next.js docs call the equivalent of API Routes in the pages directory.

  • Next.js documents proxy.ts as the request proxy file convention; older sources call the same idea middleware. NestJS keeps the term middleware for its own request pipeline.

  • The NestJS docs were reorganized in September 2026 into groups such as Application, Data, Security and HTTP; links in older articles may still point to the former /techniques/ paths.