Skip to content

What Is NestJS? The Structured TypeScript Framework for Node.js

What is NestJS? A structured TypeScript framework with modular architecture and dependency injection for building scalable, enterprise Node.js applications.

Tuan Tran Van
9 min read
Contents (10 sections)
  1. What is NestJS?
  2. What does a NestJS application consist of?
  3. How does dependency injection work in NestJS?
  4. What layers does a request pass through?
  5. Under the hood: Express, Fastify and TypeScript decorators
  6. What NestJS 11 brought
  7. Project structure and the mistakes teams make
  8. NestJS or Express — which should you pick?
  9. Where should you start with NestJS?
  10. References

NestJS is an MIT-licensed, progressive Node.js framework built with TypeScript that provides a modular architecture for developing efficient and scalable server-side applications.

When engineers ask what is NestJS, it is best defined as a platform that standardizes backend structure while maintaining compatibility with both TypeScript and pure JavaScript (via Babel).

Internally, the framework acts as an abstraction layer over mature HTTP servers, defaulting to Express while offering an optional integration with Fastify for high-performance requirements.

NestJS brings architectural order to the free-form, unopinionated Node.js ecosystem

What is NestJS?

NestJS was created in 2017 by Kamil Myśliwiec to solve the "architecture problem" prevalent in the Node.js ecosystem. While Node provides extensive libraries, it lacks a consistent standard for organizing code, which often leads to unmaintainable "spaghetti" backends. NestJS addresses this by bringing consistency and scalability to server-side development through an opinionated, highly structured environment.

The framework is heavily influenced by Angular, adopting its use of modules, providers, and decorators. It successfully synthesizes concepts from Object-Oriented Programming (OOP), Functional Programming (FP), and Functional Reactive Programming (FRP). This combination allows you to build applications that are not only type-safe but also modular and easy to test.

As a platform-agnostic framework, NestJS uses an adapter pattern to wrap underlying HTTP engines. By default, it uses Express, but you can swap this for Fastify if your application requires maximum throughput. You can access the underlying platform APIs by passing specific types—such as NestExpressApplication or NestFastifyApplication—to the NestFactory during the initialization process.

To implement NestJS, you must use Node.js version 20 or higher. The framework relies fundamentally on TypeScript decorators and metadata via the reflect-metadata package to wire the various application components together at runtime.

What does a NestJS application consist of?

A NestJS application is organized around three primary pillars: Modules, Controllers, and Providers. These components interact to form a cohesive system where responsibilities are clearly delineated.

The three pillars of a NestJS application: Modules, Controllers and Providers, and how they relate

Modules define the logical boundaries of your application. Every project starts with a root module, typically AppModule, which is the entry point for what Nest calls the "application graph." This graph is an internal map the framework uses to resolve relationships between all defined components. You use the @Module() decorator to organize your code into feature sets, specifying which controllers and providers belong to that specific domain.

typescript
@Module({
  imports: [CatsModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Controllers function as the routing layer, handling incoming HTTP requests and returning responses. By using decorators like @Controller('path'), you define route prefixes, while method decorators such as @Get(), @Post(), and @Body() allow you to extract request data with minimal boilerplate. Controllers should remain lean, focusing on request handling and delegating complex logic to the service layer.

Providers encompass classes such as services, repositories, and helpers. The defining characteristic of a provider is that it can be injected into other classes. You use the @Injectable() decorator to signal the Nest Inversion of Control (IoC) container that the class should be managed and instantiated by the framework.

typescript
interface Cat {
  name: string;
  age: number;
  breed: string;
}
 
@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];
 
  create(cat: Cat) {
    this.cats.push(cat);
  }
 
  findAll(): Cat[] {
    return this.cats;
  }
}

How does dependency injection work in NestJS?

NestJS uses an Inversion of Control (IoC) container to manage the "wiring up" of objects. The standard pattern is constructor-based injection, where you declare dependencies in the class constructor and the framework automatically resolves and injects them by type.

NestJS IoC container injecting providers into the classes that need them, with the Singleton, Request and Transient scopes

Providers operate within specific "scopes" that determine their lifetime. The default is the Singleton scope, where a single instance is shared application-wide. You may also use Request scope, which creates a new instance for every request, or Transient scope, which provides a fresh instance for every consumer. You must be cautious with Request-scoped providers; they can degrade performance because they force the entire dependency chain to be recreated for every incoming request.

Custom providers offer flexibility beyond standard class injection. You can use useValue for constant objects or mock data, useClass for swapping implementations during testing, and useFactory for asynchronous initialization or dynamic configurations that require dependencies from other services.

While constructor injection is preferred for clarity, Nest also supports property-based injection using the @Inject() decorator. This is useful in complex inheritance scenarios to avoid passing multiple dependencies through super(). Additionally, the @Optional() decorator allows you to define dependencies that do not necessarily need to be resolved for the application to function.

typescript
// Constructor-based injection vs a custom provider token
constructor(
  private readonly catsService: CatsService, // Standard injection
  @Optional() @Inject('HTTP_OPTIONS') private readonly options: any // Custom token
) {}

What layers does a request pass through?

Every request in a NestJS application follows a rigorous lifecycle. It begins with Middleware, which you use for cross-cutting concerns like logging or body-parsing. Middleware executes before the specialized Nest layers.

The NestJS request lifecycle running through Middleware, Guards, Interceptors and Pipes before reaching the Controller

The next layer is Guards. By implementing the CanActivate interface, Guards handle authentication and authorization logic. They determine if a request should proceed; if they return false or throw an UnauthorizedException, the request is blocked before it ever reaches a controller.

Interceptors wrap the request and response stream using RxJS. You use them to transform responses—such as wrapping them in a uniform JSON envelope—or to measure execution time. Interceptors provide the ability to add logic both before and after the execution of the route handler.

Pipes are used for data transformation and validation. The built-in ValidationPipe, paired with the class-validator library, ensures that incoming Data Transfer Objects (DTOs) are correctly typed and validated. If the data is invalid, the Pipe throws a BadRequestException. Finally, Exception Filters catch any unhandled exceptions, formatting them into consistent JSON responses to prevent internal stack traces from leaking to the client.

Under the hood: Express, Fastify and TypeScript decorators

NestJS is designed for flexibility at the engine level. While @nestjs/platform-express is the default because of its massive ecosystem, you can switch to @nestjs/platform-fastify to achieve significantly higher throughput.

NestJS as an abstraction layer over the underlying HTTP engine, swappable between Express and Fastify

The framework relies on TypeScript decorators to read metadata at runtime. The reflect-metadata package is essential here, as it allows Nest to identify the relationships between modules, controllers, and providers. The application bootstraps in the main.ts file, where NestFactory.create(AppModule) triggers the resolution of the entire application graph.

typescript
// main.ts bootstrap example
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // Default port configuration
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

What NestJS 11 brought

Released in early 2025, NestJS 11 introduced major internal optimizations. The most significant change was the overhaul of the module opaque key generation. By switching from slow hash-based serialization to object references, the framework significantly improved startup times for large-scale applications.

The ConsoleLogger now includes built-in JSON logging support, which is critical for modern containerized environments. While JSON logging disables terminal colors by default to keep logs clean for parsers, you can re-enable them for local development by setting colors: true in the options.

typescript
const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    json: true,
    colors: true, // Re-enable for local readability
  }),
});

Microservice transporters (NATS, Kafka, Redis) gained the unwrap() method, providing direct access to the native client for low-level configurations. A new status observable also allows for real-time connection monitoring. NestJS 11 also supports Express v5 and Fastify v5. In Express v5, wildcards now require explicit naming, such as @Get('users/*splat') instead of @Get('users/*'). Note that splat is just a naming convention; you can use any identifier, such as *wildcard.

Project structure and the mistakes teams make

You should use a "feature-based" folder structure. Grouping controllers, services, and DTOs by domain (e.g., /users, /orders) is superior for maintainability. Avoid the "layered" structure (placing all controllers in one folder and services in another), as it becomes a navigational nightmare in large projects.

Comparing two NestJS project folder layouts: feature-based versus layered

A common pitfall is the strictNullChecks trap. Nest's default tsconfig.json often leaves this disabled, which can result in a minefield of null reference errors. You should enable strict null checks immediately during project setup. Additionally, while forwardRef() can resolve circular module dependencies, frequent use of it usually indicates a flaw in your architectural design.

Finally, never expose database Entities directly from your controllers. This leaks internal schema details and tightly couples your API to your database. Use Data Transfer Objects (DTOs) for the API contract. DTOs and Entities may look identical at first, but they always diverge as the application matures.

NestJS or Express — which should you pick?

The choice depends on your project scale. Express is un-opinionated and flexible, making it suitable for lightweight applications or small, highly specialized teams. However, that lack of structure often leads to "messy" code as the application grows in complexity.

NestJS versus Express: built-in structure for large teams against maximum freedom for small projects

NestJS is the preferred choice for enterprise-level applications and large-scale systems. It provides the consistency required for multi-developer teams to work efficiently. However, there is a "hidden cost" to NestJS: the framework wiring—modules, decorators, and providers—can introduce more development overhead than it returns in value for tiny projects with only 2 to 5 developers.

Where should you start with NestJS?

To start, install the Nest CLI globally using npm i -g @nestjs/cli and create a new project with nest new project-name. The CLI is your primary tool for scaffolding consistent, boilerplate-free code.

For any production-bound project, use the --strict flag during creation. This avoids technical debt by enforcing a stricter TypeScript feature set from day one, so you use the modular structure as intended.

References

Share this article