Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

NestJS  Module 1 – Getting started with NestJS 12

What NestJS is and when to choose it

What NestJS adds on top of Express or Fastify, what it costs, and how to choose between Nest, Express, Fastify and Hono for a team and a workload.

  • Beginner
  • 15 minutes
  • Examples run with Node.js 24.21.0, NestJS 12.1.2 and TypeScript 6.0.3
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how Nest layers modules, dependency injection and decorators over Express or Fastify
  • Compare one endpoint written with Express, Fastify and Nest by files, lines and behaviour
  • Choose between Nest, Express, Fastify and Hono for a given team and workload
  • Read npm's dist-tags and engines before choosing a Nest version
What you’ll learn

What you will learn

  • Explain how Nest layers modules, dependency injection and decorators over Express or Fastify
  • Compare one endpoint written with Express, Fastify and Nest by files, lines and behaviour
  • Choose between Nest, Express, Fastify and Hono for a given team and workload
  • Read npm's dist-tags and engines before choosing a Nest version
On this page

One POST /orders endpoint that checks its body and answers 201 or 400 takes 13 lines in one file with Express 5, and 45 lines in six files with NestJS 12, plus a compile step. Those counts come from the three versions this lesson runs side by side, and they raise the question it answers: what do the extra files give you, and when is Nest worth them?

You will see what Nest adds on top of Express or Fastify, follow one request through a Nest app, send the same three requests to all three versions, choose between Nest, Express, Fastify and Hono for a team and a workload, and read npm’s release tags before you pick a version.

Nest runs on top of Express or Fastify

Nest is not an HTTP server of its own. At start-up it reads your classes, then registers one route for each handler on an HTTP library underneath: Express 5 by default, or Fastify 5. That library parses requests and writes responses; the package @nestjs/platform-express 12.1.2 depends on express 5.2.1, and @nestjs/platform-fastify on Fastify. Nest calls this layer the HTTP adapter: The part of Nest that wraps an HTTP library, Express or Fastify. Nest registers its routes on the library through it, and the library parses requests and writes responses. @nestjs/platform-express is the default..

On top of the adapter, Nest adds four things that a plain Express app leaves to you:

  • Modules. A module: A class marked with @Module() that lists the controllers and providers that belong together, the modules it imports and the providers it shares with them. Every Nest app starts from one root module. lists the controllers and providers that belong together, so a feature lives in one place and states what it shares.
  • Dependency injection. A class asks for what it needs in its constructor, and Nest’s container builds it and passes it in. This is dependency injection: A way of building objects in which a class asks for what it needs in its constructor and a container creates those objects and passes them in, instead of the class creating them itself., and the classes it builds are provider: Anything Nest's container can create and inject, most often a class marked with @Injectable() such as a service. By default Nest creates one instance of each provider and shares it..
  • Decorators for routes. @Controller('orders') and @Post() on a class and a method tell Nest which requests the method handles; such a class is a controller: A class marked with @Controller() whose methods handle requests. Each method with a route decorator such as @Get() or @Post() becomes one route of the app..
  • A pipeline around every handler. Middleware, guards, interceptors, pipes and exception filters run before and after your method, in a fixed order. A pipe: A class Nest runs on a handler's argument before the handler, to check it or turn it into another type. A pipe that throws stops the request with an error response., for example, checks an argument before the method sees it.

Step through one request of this lesson’s Nest version, a valid order, and watch which of these layers runs where:

Interactive · stage by stage One POST /orders through a Nest app on Express Step through one request. Each stage says whose code runs there, Express's, Nest's or yours; the side branch is the path of a body that fails the schema.
One POST /orders through a Nest app on Express, step 8 of 8. Showing HTTP request arrives, Express 5 (the adapter), Nest's router, Validation pipe, OrdersController, OrdersService, Response (201 {"id":1,…,"meals":3}). In focus: Response.400 when the body failsHTTP request arrivesPOST /orders, JSON bodyExpress 5 (the adapter)parses the JSON bodyNest's routerpicks the handlerValidation pipechecks createOrderSchemaOrdersControllercreate(input)OrdersServiceassigns the idResponse201 {"id":1,…,"meals":3}

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The request. A client sends POST /orders with the body {"kitchenId":"pune-07","meals":3}. No code of yours has run yet: the request first meets the HTTP server that Express owns.
  2. Express. Express 5 receives the request and parses the JSON body, because Nest registered Express's body parser when the app started. Express also adds the X-Powered-By header, which is why a Nest response says Express.
  3. Nest's router. At start-up Nest read the @Controller('orders') and @Post() decorators and registered one Express route for them. POST /orders matches it, so the request belongs to OrdersController.create.
  4. The pipe. Before the method runs, Nest runs the pipes of its parameters. StandardSchemaValidationPipe reads the schema that @Body({ schema }) attached and checks the body. meals is 3, a whole number from 1 to 10, so the body passes.
  5. A body that fails. With meals 11 the pipe throws instead, and Nest answers 400 with the problems listed, without calling your method. That is the side branch: the controller never sees invalid input.
  6. Your controller. Nest calls create(input) on the one OrdersController it built at start-up. The method holds no rule of its own; it hands the input to OrdersService.
  7. Your service. OrdersService was created once by Nest's container and passed to the controller's constructor. It gives the order its id and returns it.
  8. The response. The method returned an object, so Nest turns it into JSON, and because the method is a POST it answers 201 Created. Express writes the bytes back to the client.

Two things in that walk are easy to miss. Your controller never touched the socket: Express parsed the body and wrote the reply, Nest chose the method and ran the pipe. And the method never saw the invalid body of the side branch, because the pipe answered 400 first. The rest of this track teaches each of these layers in turn.

Checkpoint

Check: What Nest adds on top of Express

  1. Question 1 of 2 In this lesson's Nest version, which code writes the response bytes back to the client?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Express 5, the HTTP adapter underneath Nest

    Nest is not an HTTP server of its own. It reads your decorators, registers one route per handler on Express (or Fastify) and hands the value your method returns to that library, which parses requests and writes responses.

  2. Question 2 of 2 A client sends POST /orders with {"kitchenId":"pune-07","meals":11} to the Nest version. Which of your methods run?

    Choose one answer.

    A hint

    Follow the side branch of the figure.

    Show the answer: Question 2 of 2

    Answer: None of them; the validation pipe answers 400 before create() is called

    Pipes run on a handler's arguments before the handler. StandardSchemaValidationPipe checks the body against createOrderSchema; with meals 11 it throws, and Nest's exception layer answers 400 with the problems listed. Neither create method runs.

The same endpoint, written three ways

Here is POST /orders for a tiffin service, TiffinTrail: a kitchen id and a number of meals from 1 to 10. Express 5 leaves the check to you, so the handler tests each field by hand:

express/orders.mjs JavaScript · three-ways/express/orders.mjs
// POST /orders in Express 5: parse the JSON body, check it by hand, answer 201 or 400.
import express from 'express';

export const app = express();
app.use(express.json());

let nextId = 1;

app.post('/orders', (req, res) => {
  const { kitchenId, meals } = req.body ?? {};
  const valid = typeof kitchenId === 'string' && Number.isInteger(meals) && meals >= 1 && meals <= 10;
  if (!valid) {
    res.status(400).json({ message: 'kitchenId must be text and meals a whole number from 1 to 10' });
    return;
  }
  res.status(201).json({ id: nextId++, kitchenId, meals });
});

Fastify 5 takes a JSON Schema on the route and checks the body before the handler runs:

fastify/orders.mjs JavaScript · three-ways/fastify/orders.mjs
// POST /orders in Fastify 5: a JSON Schema on the route, checked by Fastify before the handler runs.
import Fastify from 'fastify';

export const app = Fastify();

let nextId = 1;

const body = {
  type: 'object',
  required: ['kitchenId', 'meals'],
  properties: {
    kitchenId: { type: 'string' },
    meals: { type: 'integer', minimum: 1, maximum: 10 },
  },
};

app.post('/orders', { schema: { body } }, async (request, reply) => {
  const { kitchenId, meals } = request.body;
  reply.code(201);
  return { id: nextId++, kitchenId, meals };
});

The Nest version splits the same work into a schema, a provider, a controller and a module, plus app.ts, which switches on the validation pipe, and main.ts, which starts the server. The schema is a Zod object; Nest 12 checks any Standard Schema: A common interface that validation libraries such as Zod, Valibot and ArkType implement, so that a framework can check data against a schema from any of them. Nest 12's StandardSchemaValidationPipe uses it. that @Body({ schema }) names once StandardSchemaValidationPipe is on:

nest/src: the same endpoint in six files

TypeScript (orders.controller.ts) · three-ways/nest/src/orders.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { createOrderSchema, type CreateOrderInput } from './create-order.schema.js';
import { OrdersService } from './orders.service.js';

// The controller only translates HTTP: POST /orders becomes a call to OrdersService.create().
@Controller('orders')
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}

  @Post()
  create(@Body({ schema: createOrderSchema }) input: CreateOrderInput) {
    return this.orders.create(input);
  }
}

TypeScript (orders.service.ts) · three-ways/nest/src/orders.service.ts

import { Injectable } from '@nestjs/common';
import type { CreateOrderInput } from './create-order.schema.js';

// The business rule lives in a provider, which Nest creates once and hands to every class that asks for it.
@Injectable()
export class OrdersService {
  private nextId = 1;

  create(input: CreateOrderInput) {
    return { id: this.nextId++, ...input };
  }
}

TypeScript (create-order.schema.ts) · three-ways/nest/src/create-order.schema.ts

import { z } from 'zod';

// The rule for the body, written once: Nest checks it, and TypeScript derives the type from it.
export const createOrderSchema = z.object({
  kitchenId: z.string(),
  meals: z.number().int().min(1).max(10),
});

export type CreateOrderInput = z.infer<typeof createOrderSchema>;

TypeScript (app.module.ts) · three-ways/nest/src/app.module.ts

import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller.js';
import { OrdersService } from './orders.service.js';

// The module lists what belongs together: Nest builds the service and gives it to the controller.
@Module({
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class AppModule {}

TypeScript (app.ts) · three-ways/nest/src/app.ts

import { StandardSchemaValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';

// Builds the application without starting it, so main.ts and the comparison can both use it.
export async function createApp() {
  const app = await NestFactory.create(AppModule, { logger: false });
  app.useGlobalPipes(new StandardSchemaValidationPipe());
  return app;
}

TypeScript (main.ts) · three-ways/nest/src/main.ts

import { createApp } from './app.js';

const app = await createApp();
await app.listen(process.env.PORT ?? 3000);

The session below compiles the Nest version, starts all three on ports the system picks, sends each the same three bodies, and counts the files and lines of each version:

Three versions, the same three requests JavaScript · three-ways/run.mjs
import { session } from '../session.mjs';

// Compiles the Nest version, sends the same three requests to all three, then counts their files and lines.
session(`
npx tsc -p nest/tsconfig.json
node compare.mjs
node sizes.mjs
`);

Output

$ npx tsc -p nest/tsconfig.json
$ node compare.mjs
POST /orders {"kitchenId":"pune-07","meals":3}
  express  201 {"id":1,"kitchenId":"pune-07","meals":3}
  fastify  201 {"id":1,"kitchenId":"pune-07","meals":3}
  nest     201 {"id":1,"kitchenId":"pune-07","meals":3}
POST /orders {"kitchenId":"pune-07","meals":11}
  express  400 {"message":"kitchenId must be text and meals a whole number from 1 to 10"}
  fastify  400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body/meals must be <= 10"}
  nest     400 {"message":["meals: Too big: expected number to be <=10"],"error":"Bad Request","statusCode":400}
POST /orders {"kitchenId":"pune-07","meals":"3"}
  express  400 {"message":"kitchenId must be text and meals a whole number from 1 to 10"}
  fastify  201 {"id":2,"kitchenId":"pune-07","meals":3}
  nest     400 {"message":["meals: Invalid input: expected number, received string"],"error":"Bad Request","statusCode":400}
$ node sizes.mjs
express  1 file   13 lines  orders.mjs
fastify  1 file   16 lines  orders.mjs
nest     6 files  45 lines  app.module.ts, app.ts, create-order.schema.ts, main.ts, orders.controller.ts, orders.service.ts

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs

The first body passes everywhere and the second fails everywhere, each with its own error shape. The third is the surprise: "meals": "3" is text, yet Fastify answered 201 and stored the number 3, because Fastify configures its JSON Schema validator with coerceTypes: 'array', which turns "3" into 3 before checking it. The hand-written check and Zod’s z.number() reject it with 400.

Version Files Lines of code Where the check lives "meals": "3"
Express 5 1 13 your handler 400
Fastify 5 1 16 a JSON Schema on the route 201, as the number 3
NestJS 12 6 45 a Zod schema, run by a pipe 400

Taking “validated” to mean the same thing everywhere

Three frameworks, three answers to one request. Before you move an API from one framework to another, send it the same edge cases on both, as this session does, and compare status codes and bodies: a coercion or a missing check changes what your database receives without any error.

REST API Tester (Online API Client) Send your own bodies to a local server and compare status codes and responses.

Checkpoint

Check: One endpoint in three frameworks

  1. Question 1 of 2 Which versions answered 201 to {"kitchenId":"pune-07","meals":"3"}, where meals is text?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Fastify only, and it stored meals as the number 3

    The recorded run shows 400 from Express and Nest and 201 from Fastify. Fastify's default validator settings coerce values to the types the schema names, so a text "3" passes an integer check as the number 3.

  2. Question 2 of 2 How many lines of code did sizes.mjs count for the Nest version, across its six files?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 45 lines

    sizes.mjs counted 13 lines for Express, 16 for Fastify and 45 lines in six files for Nest, not counting blank lines or lines that hold only a comment.

What the extra files buy, and what they cost

Nest’s six files are not six times the work, because each one has a single job and a fixed place. A new person on the team finds the rule for orders in OrdersService, the HTTP mapping in OrdersController and the wiring in OrdersModule, in every feature of every Nest app. The same structure carries over to GraphQL resolvers, WebSocket gateways and message-broker handlers, which use the same modules, providers and pipes. And because the controller receives its service through the constructor, a test can hand it a fake service without starting a server.

The costs are real, too:

Nest gives you Nest costs you
One layout for every feature: modules, controllers, providers More files per feature: 6 against 1 for this endpoint
Injection, so classes can be tested with fakes Concepts to learn first: modules, providers, scopes, pipes
Validation, guards and error handling in fixed places A compile step, because decorators are not plain JavaScript
One programming model for HTTP, GraphQL, sockets and brokers Start-up work: Nest reads every module before the first request

The compile step deserves a word now, because it surprises people who run TypeScript directly with Node.js. Express and Fastify code is plain JavaScript, so node orders.mjs runs it. Nest code needs a TypeScript compiler to turn its decorators into function calls and to record the types of constructor parameters, which is why the session above ran npx tsc first. The last lesson of this module shows exactly what that compiler writes.

Key idea

Nest trades more files and a build step for a fixed shape, injection and a shared pipeline. The trade pays off as the team, the codebase and the number of transports grow.

Checkpoint

Check: What the extra files buy and cost

  1. One question Why did the session run npx tsc before node compare.mjs?

    Choose one answer.

    Show the answer: One question

    Answer: The Nest version's decorators and types must be compiled to JavaScript before Node.js can run it

    Nest's code uses decorators and needs the types of constructor parameters recorded, so it is compiled with TypeScript (or SWC) before it runs. That build step is one of the costs of Nest.

Choosing Nest, Express, Fastify or Hono for your case

The decision mostly comes down to three questions about your service. Answer them in the table below; it marks the frameworks your answers rule out or warn about, and the columns that decide:

Interactive · answer and compare Which framework fits your service? Answer three questions about your service. The table marks what each answer rules out or warns about, and the columns that decide it.
How many people will work on this service over its life?
Does it need more than an HTTP API: GraphQL, WebSockets or a message broker?
Where must it run?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

NestJS 12, Express 5, Fastify 5 and Hono against five things that decide the choice
Framework Structure for a team Beyond HTTP Where it runs Checking input Build and size
NestJS 12 Good modules, injection and one shape for every feature Good one model for HTTP, GraphQL, WebSockets and message brokers Mixed a Node.js server, through Express or Fastify Good pipes check a body against a schema before your method runs Poor a compile step, and 6 files with 45 lines for the lesson's endpoint
Express 5 Mixed no prescribed layout: each team chooses its own folders and wiring Poor HTTP only; sockets and queues are separate libraries you wire up Mixed a Node.js server Mixed by hand or with a library you add; nothing is checked by default Good 1 file with 13 lines for the same endpoint, and no build
Fastify 5 Mixed plugins with their own scope, but no prescribed layout Mixed HTTP first; WebSockets and the rest through plugins Mixed a Node.js server Good a JSON Schema per route, checked before the handler, with type coercion on Good 1 file with 16 lines for the same endpoint, and no build
Hono Mixed a small core with routers and middleware; the layout is yours Poor HTTP handlers on the web-standard Request and Response Good the same code on Node.js, Deno, Bun, Fastly Compute and AWS Lambda Mixed validator middleware that you add to each route Good one small package with no dependencies, and no decorator build
How the answers decide
  • “Where must it run?” An edge or web-standard runtime: rules out NestJS 12, Express 5 and Fastify 5. Express and Fastify, and so Nest on top of them, need a Node.js HTTP server. Hono's handlers use the Request and Response that edge runtimes provide.
  • “How many people will work on this service over its life?” One or two people and “Does it need more than an HTTP API: GraphQL, WebSockets or a message broker?” No, an HTTP API: adds a caveat to NestJS 12. For one or two people and one HTTP API, Nest's modules, injection and build step can cost more than they save. Express or Fastify is less to learn.
  • “How many people will work on this service over its life?” A team, or several teams: adds a caveat to Express 5, Fastify 5 and Hono. Without a prescribed layout, each team settles its own folders, wiring and error format. Nest gives every feature the same shape.
  • “Does it need more than an HTTP API: GraphQL, WebSockets or a message broker?” Yes, or soon: adds a caveat to Express 5, Fastify 5 and Hono. GraphQL, sockets and brokers each bring their own library and style. Nest runs them with the same modules, providers and pipes as HTTP.

In short:

  • NestJS fits a team that will grow, a codebase that will live for years, or a service that will speak more than HTTP. It runs on a Node.js server through Express or Fastify.
  • Express fits a small HTTP service or a quick prototype, and code that already depends on Express middleware. Express 5 is the library Nest itself uses by default.
  • Fastify fits an HTTP service that wants checks declared per route and fast JSON handling with little code. Remember that it coerces types unless you configure its validator otherwise.
  • Hono fits code that must run on an edge or web-standard runtime, such as Fastly Compute, Deno or Bun. Its handlers take the web-standard Request and return a Response.

Checkpoint

Check: Choosing a framework for a case

  1. Question 1 of 2 A small API must run on an edge runtime close to users, one that offers web-standard Request and Response but no Node.js HTTP server. Which framework fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Hono

    The runtime decides first. Express and Fastify, and so Nest on top of them, run on a Node.js server; Hono is built on web standards and runs on edge runtimes as well as on Node.js.

  2. Question 2 of 2 Two developers will build one internal HTTP API with a dozen endpoints, and no other transport is planned. What is the soundest choice?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Express or Fastify, unless the team expects to grow

    With a small team, one transport and no plan to grow, the simpler frameworks win on cost. If the API is likely to grow into several services, or a team will maintain it for years, Nest's structure starts to pay.

Read the release line before you start

npm keeps a few named pointers for each package, its dist-tag: A name on the npm registry, such as latest or legacy, that points at one version of a package. npm install name@tag installs that version, and maintainers move tags by hand.. npm install @nestjs/core installs the version latest points at, and npm install @nestjs/core@legacy the one legacy points at. npm publish moves latest to each new release unless the maintainer names another tag, and every other tag moves only when a maintainer moves it, so a tag can be stale. This program reads a saved copy of the registry’s metadata for @nestjs/core and prints where each tag points and what each major line declares:

Where the dist-tags of @nestjs/core point JavaScript · release-line/release_line.mjs
// Reads a saved copy of npm's metadata for @nestjs/core (registry-snapshot.json) and prints where each dist-tag
// points, then, for each major line, its stable releases, the newest one and what their engines fields declare.
import { readFileSync } from 'node:fs';

const meta = JSON.parse(readFileSync('registry-snapshot.json', 'utf8'));

function parse(version) {
  const [, major, minor, patch, pre = ''] = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(version);
  return { major: Number(major), minor: Number(minor), patch: Number(patch), pre };
}

// Older first; a pre-release comes before the release it leads to (12.0.0-alpha.7 < 12.0.0).
function compare(a, b) {
  const x = parse(a);
  const y = parse(b);
  return x.major - y.major || x.minor - y.minor || x.patch - y.patch || (x.pre === y.pre ? 0 : x.pre === '' ? 1 : y.pre === '' ? -1 : x.pre < y.pre ? -1 : 1);
}

const stable = Object.keys(meta.versions).filter((v) => parse(v).pre === '').sort(compare);
const newestOf = (major) => stable.filter((v) => parse(v).major === major).at(-1);
const latest = meta['dist-tags'].latest;

console.log('dist-tags of @nestjs/core, newest version first:');
const tags = Object.entries(meta['dist-tags']).sort(([, a], [, b]) => compare(b, a));
for (const [tag, version] of tags) {
  const { major, pre } = parse(version);
  let note = '';
  if (pre) note = compare(version, latest) < 0 ? 'a pre-release, older than latest' : 'a pre-release';
  else if (newestOf(major) !== version) note = `${newestOf(major)} is newer on the same line`;
  console.log(`  ${tag.padEnd(7)} ${version.padEnd(15)} ${note}`.trimEnd());
}

// Every stable release of a line is read, so "all declare" is checked, not assumed from the newest one.
console.log('Stable releases of each major line, and what their engines fields declare:');
for (const major of [12, 11, 10]) {
  const line = stable.filter((v) => parse(v).major === major);
  const declared = [...new Set(line.map((v) => meta.versions[v].engines?.node ?? null))];
  let engines;
  if (declared.length > 1) engines = `mixed: ${declared.map((d) => d ?? 'none').join(', ')}`;
  else engines = declared[0] === null ? 'none declares engines' : `all declare node ${declared[0]}`;
  console.log(`  ${major}.x  ${String(line.length).padStart(2)} releases, newest ${newestOf(major).padEnd(8)} ${engines}`);
}

Output

dist-tags of @nestjs/core, newest version first:
  latest  12.1.3
  next    12.0.0-alpha.7  a pre-release, older than latest
  legacy  11.2.7
  old     10.4.19         10.4.22 is newer on the same line
  beta    9.3.0-beta.3    a pre-release, older than latest
  rc      6.0.0-rc.1      a pre-release, older than latest
Stable releases of each major line, and what their engines fields declare:
  12.x   9 releases, newest 12.1.3   all declare node >= 20
  11.x  60 releases, newest 11.2.7   all declare node >= 20
  10.x  55 releases, newest 10.4.22  none declares engines

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node release_line.mjs

Read it from the top. latest is 12.1.3. next, beta and rc are old pre-releases that nobody moved forward, so npm install @nestjs/core@next would install an alpha of 12.0 that is older than every 12.x release. old points at 10.4.19 although 10.4.22 is newer on the same line; npm install @nestjs/core@10 asks for the newest 10.x instead. The engines field is the other thing to read: every 11.x and 12.x release declares node >= 20, and no 10.x release declares anything.

Walk through the three lines, oldest first, to see what each tag stands for:

Timeline · 5 events in order The release line of @nestjs/core Walk through the major lines from the oldest. Each event says which npm tag points at the line and what the line changed.
  1. 10.x

    The line npm calls old old tag

    The old tag points at 10.4.19, although 10.4.20, 10.4.21 and 10.4.22 were published later on the same line. No 10.x package declares the Node.js versions it needs.

  2. 11.x

    The line npm calls legacy legacy tag

    11.2.7 carries the legacy tag, the newest 11.x for apps that have not moved to 12 yet. Every 11.x release declares node >= 20 in its engines field.

  3. 12.0

    ES modules and new defaults major

    The Nest packages ship as ES modules, and nest new asks for an ESM project with Vitest or a CommonJS one with Jest. Route conflict checks, Standard Schema validation and machine-readable error codes arrive.

  4. 12.1

    Cookies, CSRF checks and security headers minor

    Cookies, CSRF protection and security headers become built-in features that work on both Express and Fastify.

  5. 12.1.3

    The release npm calls latest latest tag

    The latest tag points at 12.1.3, a patch release. The examples of this track run on 12.1.2, one patch earlier.

The timeline has 5 events, from 10.x to 12.1.3.

The engines field is a floor, not the whole story. The migration guide asks for Node.js 20.19 or later, or 22.12 or later on the 22 line, to run a Nest 12 app, and the CLI’s code generators ask for 22.22.3, 24.15 or 26. The next lesson starts a project with Node.js 24.21.0, which meets both.

How widely Nest is used

In Stack Overflow’s 2025 developer survey, 6.7 % of all respondents said they had done extensive development work in NestJS over the past year, against 19.9 % for Express and 2.9 % for Fastify. A survey measures who answered it, not the whole industry, so treat the numbers as a rough guide to how many people you can ask for help.

Checkpoint

Check: Reading dist-tags and engines

  1. Question 1 of 2 You start a new project and want the current stable 12.x. Which command installs it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: npm install @nestjs/core

    latest is the tag npm installs by default. The program shows next, beta and rc pointing at old pre-releases, and legacy and old at the 11.x and 10.x lines.

  2. Question 2 of 2 An app must stay on Nest 10 for now. Which install gets the newest 10.x patch?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: npm install @nestjs/core@10

    Tags are pointers that maintainers move by hand, and old was not moved after 10.4.19. Asking for the range 10 lets npm choose the newest 10.x, which the program prints as 10.4.22.

Practise: pick the framework

Each case below names a team, a workload and one constraint. Pick the framework that fits, then compare your reason with the explanation under the answer:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 @nestjs/platform-express 12.1.2 is the default HTTP adapter of Nest 12. Which library does it depend on?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: Express 5.2.1

    @nestjs/platform-express 12.1.2 lists express 5.2.1 as a dependency. Nest registers its routes on Express, and Express does the HTTP work.

  2. Question 2 of 10 A colleague says that moving to Nest means giving up Express and its middleware. What is the most accurate reply?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Nest runs on Express by default, so Express middleware can still be used; Nest adds structure on top

    Nest sits on top of an HTTP library and exposes it. On the default Express adapter, existing Express middleware still works; what Nest adds is modules, injection, route decorators and the pipeline around handlers.

  3. Question 3 of 10 In the Fastify version, where is the body checked?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: In a JSON Schema passed with the route, before the handler runs

    Fastify validates request parts against JSON Schemas given with the route. It answered 400 with code FST_ERR_VALIDATION when meals was 11.

  4. Question 4 of 10 You move the Fastify version of POST /orders to Nest with the Zod schema of this lesson. A mobile app that sends "meals": "2" starts getting 400. What explains it?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Fastify had been coercing the text to a number, and z.number() does not

    The run shows it: Fastify answered 201 to "3" and stored 3, Nest with Zod answered 400. When you change framework, send the old edge cases to the new version and decide on purpose whether to coerce, for example with z.coerce.number(), or to ask clients to send numbers.

  5. Question 5 of 10 Which of these are costs of choosing Nest that this lesson names? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 5 of 10

    Answer:

    • More files per feature
    • A compile step before Node.js can run the code
    • Concepts to learn first, such as modules, providers and pipes

    The lesson names more files per feature, concepts to learn first, a compile step and start-up work. Running on Express is not a cost: it is how Nest works by default.

  6. Question 6 of 10 Why does dependency injection make OrdersController easier to test?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: The controller receives OrdersService through its constructor, so a test can pass a fake service

    A class that asks for its dependencies in its constructor can be given different ones. A test builds the controller with a fake OrdersService and checks what it calls, without HTTP or a database.

  7. Question 7 of 10 A team of eight starts an order service that will add a message broker next quarter and a GraphQL API later. Which choice fits best?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: NestJS, because it gives a growing team one structure across HTTP, brokers and GraphQL

    The table's rules point the same way: a team rules the unstructured frameworks out with a caveat, and more transports favour Nest's single programming model.

  8. Question 8 of 10 An existing Express 5 app with ten custom middleware functions must gain modules and injection without a rewrite of its middleware. What is the least risky path?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Move it to Nest on the Express adapter and keep the middleware, applied through Nest

    Choosing the adapter is part of the choice. Express middleware keeps working on Nest's default Express adapter; moving to Fastify for speed is a separate project.

  9. Question 9 of 10 In the snapshot, where does the legacy dist-tag of @nestjs/core point?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: 11.2.7, the newest 11.x

    The program printed legacy 11.2.7. Tags are names for versions: latest, legacy and old here, plus three stale pre-release tags.

  10. Question 10 of 10 A CI image has Node.js 22.12.0. What can it do with Nest 12?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Run a Nest 12 app, but not run the CLI's generators such as nest new

    Nest 12 separates the two: running an app needs Node.js 20.19+ (22.12+ on the 22 line), and the CLI's generators need 22.22.3+, 24.15+ or 26+. The engines field of @nestjs/core says only node >= 20.

Interview questions

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid · Warm-up

    What does NestJS add on top of Express, and what does Express still do in a Nest app?

    Express still does the HTTP work: on Nest's default adapter it parses each request and writes each response, which is why a Nest response says X-Powered-By: Express. Nest registers one Express route per handler at start-up and adds four things around them. Modules group controllers and providers into features. A dependency-injection container builds each provider once and passes it to the classes whose constructors ask for it. Decorators such as @Controller() and @Post() declare the routes. And a pipeline of middleware, guards, interceptors, pipes and exception filters runs around every handler, so checks and errors live in fixed places. The cost is a compile step and more files: in this lesson's comparison, 45 lines in six files against 13 lines in one Express file.

  2. Mid to senior

    Your team of eight is starting an order service. It needs a REST API now, will consume a message broker next quarter, and must be maintained for years. Would you build it with NestJS or plain Express 5? Defend the choice, and say what it will cost the team.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    The body {"kitchenId":"pune-07","meals":"3"} got 201 from a Fastify service and 400 from its NestJS replacement, which validates with Zod. Explain what happened, and how you would move existing clients to the new service without breaking them.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Nest registers its routes on Express 5 by default, or on Fastify 5; that library parses requests and writes the responses, and Nest adds modules, dependency injection, route decorators and a pipeline around every handler.
  • The same endpoint took 13 lines in Express, 16 in Fastify and 45 lines in six files in Nest, and Nest’s version needs a compile step.
  • The same body can get different answers: Fastify coerced "3" to 3 and answered 201, while the hand-written check and Zod’s schema answered 400.
  • Nest pays off for teams, long-lived code and services that speak more than HTTP; Express and Fastify suit small HTTP services, and Hono suits edge and web-standard runtimes.
  • npm’s dist-tags are pointers: npm publish moves latest, and every other tag moves only when a maintainer moves it, so check what latest, legacy and old point at, and the engines field, before you install.

Terms in this lesson

controller
A class marked with @Controller() whose methods handle requests. Each method with a route decorator such as @Get() or @Post() becomes one route of the app.
dependency injection
A way of building objects in which a class asks for what it needs in its constructor and a container creates those objects and passes them in, instead of the class creating them itself.
dist-tag
A name on the npm registry, such as latest or legacy, that points at one version of a package. npm install name@tag installs that version, and maintainers move tags by hand.
HTTP adapter
The part of Nest that wraps an HTTP library, Express or Fastify. Nest registers its routes on the library through it, and the library parses requests and writes responses. @nestjs/platform-express is the default.
module
A class marked with @Module() that lists the controllers and providers that belong together, the modules it imports and the providers it shares with them. Every Nest app starts from one root module.
pipe
A class Nest runs on a handler's argument before the handler, to check it or turn it into another type. A pipe that throws stops the request with an error response.
provider
Anything Nest's container can create and inject, most often a class marked with @Injectable() such as a service. By default Nest creates one instance of each provider and shares it.
Standard Schema
A common interface that validation libraries such as Zod, Valibot and ArkType implement, so that a framework can check data against a schema from any of them. Nest 12's StandardSchemaValidationPipe uses it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.