Back to blog
Backend Development
Intermediate

Master NestJS: Building Scalable Node.js APIs with TypeScript

Discover how NestJS simplifies the creation of robust, maintainable Node.js backends. This guide walks you through core concepts, architecture, and real-world implementation steps.

July 5, 2026

Introduction

NestJS has quickly become the go-to framework for developers who want to build scalable, modular, and maintainable Node.js applications. Leveraging TypeScript out of the box, NestJS introduces a powerful set of architectural patterns such as dependency injection, modules, and interceptors that align well with enterprise-level development. In this comprehensive guide we will dive deep into everything you need to know to get started with NestJS, from core concepts and architecture to step-by-step implementation, real-world examples, production-grade code, performance tips, security considerations, and deployment strategies.

By the end of this article you will have a solid understanding of how to design a well-structured NestJS project, apply best practices, avoid common pitfalls, and write clean, testable code. Whether you are a junior developer looking to level up or a senior engineer seeking to standardize a Node.js backend, this guide is crafted to be both practical and thorough, offering actionable examples you can copy and adapt immediately.

Table of Contents

Core Concepts

Before we write any code, we need to understand the philosophical underpinnings of NestJS. The framework is heavily inspired by Angular and applies several design patterns that promote separation of concerns and testability.

Dependency Injection

Dependency Injection (DI) is at the heart of NestJS. Services, providers, and other components can declare their dependencies, and Nest will automatically resolve and inject them. This makes testing trivial because you can mock dependencies without navigating complex constructor calls.

Modules

Modules are the building blocks of a Nest application. A module groups related features, providers, and controllers. By using the @Module decorator you define an entry point for a logical boundary. Nest will internally maintain a graph of module relationships, which helps with tree‑based injection and CLI tooling.

Controllers and Services

Controllers handle incoming HTTP requests and return responses. They should be thin and focus on request/response mapping. Services contain business logic and can be injected into controllers, other services, or even other providers. Keeping controllers thin ensures that the application's core logic stays independent of HTTP specifics.

Providers

Providers are responsible for creating and exposing values to the DI system. They can be classes, factories, or values. NestJS uses the TypeScript @Injectable() decorator to mark a class as a provider. Each provider is scoped to the application, request, or session depending on its scope setting.

Interceptors, Pipes, and Guards

Intercepting request/response flow, transforming data, and enforcing access control are all expressed through decorators: Interceptor, Pipe, and Guard. These features enable reusable cross‑cutting concerns such as logging, validation, authentication, and error handling.

Exception Filters

When an exception is thrown inside a controller, NestJS can run a filter to customize how it is handled. This is essential for returning consistent error responses and for logging errors before they bubble up.

Reflection and Metadata

Nest leverages TypeScript reflection to inspect parameter types, decorators, and class metadata. This allows the framework to automatically map route parameters, query strings, and request bodies using built‑in pipes.

CLI

The official Nest CLI provides generators for modules, controllers, services, and other entities. It also supports building, testing, and packaging the application with TypeScript transpilation and dependency management.

Understanding these core concepts is the first step toward writing maintainable NestJS code. In the next section we will explore how these pieces fit together in an architectural overview.

Architecture Overview

The NestJS architecture can be visualized as a layered system that mirrors a typical MVC (Model‑View‑Controller) pattern but adds additional abstractions for cross‑cutting concerns.

Layered View

  • Presentation Layer (Controllers): Handles HTTP requests, websockets, or gRPC messages. Controllers are entry points that define routes using standard Express.js methods.
  • Domain Layer (Services & Providers): Contains the core business logic. Services are injected into controllers and may also use other services for orchestration.
  • Data Access Layer (Repositories, TypeORM/Mongoose, etc.): Interacts with the database. Repositories abstract database calls and are injected into services.
  • Cross‑cutting Layers: Interceptors modify request/response, Pipes transform and validate input, Guards enforce policies, and Filters handle exceptions.

Modules act as a container for each layer, enabling logical separation and reducing circular dependencies. For example, a UsersModule may contain a UsersController, a UserService, a UserRepository, and several providers for validation, logging, or caching.

Dependency Injection Tree

Because NestJS is based on a hierarchical injector, the same provider can be injected at different levels with different lifetimes. Scope can be set using the ScopedProvider or Inject decorators. The default application‑wide scope is perfect for stateless services, while request‑scoped providers are useful for services that need per‑request context (e.g., request‑id, locale).

Routing and Middleware

Routing is defined with decorators (@Get, @Post, etc.) and uses Express.js under the hood, but Nest provides a unified way to declaratively define routes across modules. Middleware can be registered globally, per module, or per controller, allowing custom logic like CORS, body parsing, or third‑party integrations.

Error Handling and Logging

Exception filters are the preferred way to standardize error output. Nest ships with a default HTTP exception filter that produces JSON with status codes and error messages. For logging, Nest supports Pino, Winston, and other loggers via the Logger service.

Testing Philosophy

Thanks to dependency injection and modular design, unit tests can mock providers easily. Nest provides a testing module (TestingModule) and utilities like INestApplication for integration testing HTTP endpoints without running a server.

In practice, you will start by defining modules, then create controllers and services, then add pipes for validation, guards for authentication, and filters for error handling. The architecture naturally leads to code that is testable, maintainable, and scalable.

Step-by-Step Guide

Now we move from theory to practice. This section walks through the creation of a simple but realistic NestJS project from scratch using the official CLI. We will build an API for managing a list of products.

1. Install the CLI and Create a New Project

npm i -g @nestjs/cli
nest new product-api --plain

The --plain flag skips TypeScript and component plugins, giving a clean TypeScript project. After installation, change into the directory and run npm run start:dev to see the default Hello World application.

2. Define a Module Structure

In a Nest application, each logical feature lives in a separate module. For the product API we will create a ProductsModule.


// src/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';

@Module({
  controllers: [ProductsController],
  providers: [ProductsService],
})
export class ProductsModule {}

The module is then imported into the root AppModule (or any parent module). This is how Nest composes the dependency graph.

3. Create the Controller with Routes

Controllers define endpoints. We will implement CRUD actions: list, create, read, update, delete.


// src/products/products.controller.ts
import { Controller, Get, Post, Body, Param, Patch, Delete } from '@nestjs/common';
import { ProductsService } from './products.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get()
  findAll() {
    return this.productsService.findAll();
  }

  @Post()
  create(@Body() createProductDto: CreateProductDto) {
    return this.productsService.create(createProductDto);
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.productsService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
    return this.productsService.update(id, updateProductDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.productsService.remove(id);
  }
}

Note the use of DTOs (Data Transfer Objects). DTOs keep the request payload strictly defined and are automatically validated using Pipes.

4. Build the Service and Domain Logic

The service holds the business logic and interacts with a repository. For simplicity, we will use an in-memory array. In a real app you would inject a repository that talks to a database.


// src/products/products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private readonly productsRepository: Repository,
  ) {}

  async findAll() {
    return this.productsRepository.find();
  }

  async create(createProductDto: CreateProductDto) {
    const product = this.productsRepository.create(createProductDto);
    return this.productsRepository.save(product);
  }

  async findOne(id: string) {
    return this.productsRepository.findOneBy({ id });
  }

  async update(id: string, updateProductDto: UpdateProductDto) {
    await this.productsRepository.update(id, updateProductDto);
    return this.findOne(id);
  }

  async remove(id: string) {
    await this.productsRepository.delete(id);
    return { deleted: true };
  }
}

Here we used @InjectRepository to inject a TypeORM repository. If you prefer a simpler approach, you can just use an array and eliminate the ORM dependency.

5. Define the Product Entity

For TypeORM we need an entity class that maps to a database table.


// src/products/product.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { IsString, IsNumber } from 'class-validator';

@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  @IsString()
  name: string;

  @Column('decimal')
  @IsNumber()
  price: number;
}

Class validators (via class-validator) can be used in combination with the ValidationPipe to enforce constraints.

6. Set Up DTOs and Validation

DTOs are plain classes used to transfer data between client and server. They are often combined with class-validator for validation.


// src/products/dto/create-product.dto.ts
import { IsString, IsNumber } from 'class-validator';

export class CreateProductDto {
  @IsString()
  name: string;

  @IsNumber()
  price: number;
}

// src/products/dto/update-product.dto.ts
import { IsOptional } from 'class-validator';

export class UpdateProductDto {
  @IsOptional()
  @IsString()
  name?: string;

  @IsOptional()
  @IsNumber()
  price?: number;
}

In the controller we can enable validation globally via ValidationPipe.


// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

7. Add Interceptors and Guards (Optional but Recommended)

Interceptors can log every request. Here is a simple logging interceptor.


// src/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    const request = context.switchToHttp().getRequest();
    console.log(`[${request.method}] ${request.url} - ${new Date().toISOString()}`);
    return next.handle().pipe(tap(() => console.log('Request completed')));
  }
}

Apply globally:


app.useGlobalInterceptors(new LoggingInterceptor());

8. Create a Guard for Authentication (Example)

This guard checks for a simple API key header. Real applications will use JWT or OAuth2.


// src/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise | Observable {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];
    // For demonstration, compare with a constant.
    return apiKey === 'supersecret';
  }
}

Apply the guard to routes:


// src/products/products.controller.ts
import { AuthGuard } from '../auth.guard';
@Controller('products')
@UseGuards(AuthGuard)
export class ProductsController { ... }

9. Start the Application

Now you can run the application using

npm run start:dev

Open http://localhost:3000/products to see an empty list (since no data). Use Postman or curl to test the endpoints with a JSON body:

curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name":"Laptop","price":999.99}'

Return a JSON representation of the created product.

10. Running Tests

NestJS comes with a test runner that uses Jest. We can write a test for the service using the TestingModule.


// src/products/products.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ProductsService } from './products.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Product } from './product.entity';

describe('ProductsService', () => {
  let service: ProductsService;
  let repository: Repository;

  beforeEach(async () =>n  {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ProductsService,
        {
          provide: getRepositoryToken(Product),
          useClass: mockRepository,
        },
      ],
    }).compile();

    service = module.get(ProductsService);
    repository = module.get(getRepositoryToken(Product));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

With mocks you can assert that the service calls the repository correctly.

Real-World Examples

Applying NestJS to a real scenario helps solidify understanding. Below are three typical patterns you will encounter in production.

Example 1: E-commerce Product Catalog

An e-commerce platform needs a product catalog API that supports CRUD, filtering, pagination, and search. Using NestJS you can organize the following modules:

  • ProductsModule: contains all product-related controllers, services, and repositories.
  • CategoriesModule: manages product categories with nested routing.
  • ReviewsModule: handles user reviews using event-driven architecture (NATS or Redis).

Each module can have its own validation pipes (price must be > 0, slug uniqueness), guards (admin roles), and interceptors (caching responses). By leveraging the CLI, you can scaffold these modules with a single command and maintain a clean folder structure.

Example 2: SaaS Multi‑Tenant API

In a SaaS product, you need to isolate data per tenant. NestJS supports tenant‑aware providers using request‑scoped injection. A typical implementation includes:

  • TenantGuard: validates the tenant identifier in the request header and attaches a tenant context.
  • TenantService: returns the appropriate database connection (using TypeORM's multiple data sources).
  • TenantModule: registers the guard globally and provides the service.

By using modules, you can load different database configurations per tenant without mixing concerns.

Example 3: Real‑Time Chat Application

NestJS supports WebSockets via the @nestjs/platform-ws package. A chat app would contain:

  • ChatGateway: decorated with @WebSocketGateway and @SubscribeMessage to handle events.
  • ChatService: persists messages using a repository.
  • MessagePublisher: publishes events to a message queue (e.g., Redis) for real‑time updates to WebSocket clients.

Because NestJS provides a clean separation between WebSocket controllers and HTTP controllers, you can evolve a simple RPC service into a full real‑time platform without rewriting code.

Production Code Examples

Below is a concise, production‑ready code snippet that showcases best practices: module organization, dependency injection with TypeORM, validation, authentication via JWT, and error handling with custom exception filters.

1. Project Structure


src/
├── config/
│   ├── database.config.ts
│   └── env.config.ts
├── core/
│   ├── exceptions/
│   │   ├── http-exception.filter.ts
│   │   └── rpc-exception.filter.ts
│   └── guards/
│       └── jwt-auth.guard.ts
├── modules/
│   ├── users/
│   │   ├── users.module.ts
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   └── users.repository.ts
│   ├── auth/
│   │   ├── auth.module.ts
│   │   ├── auth.controller.ts
│   │   └── auth.service.ts
│   └── shared/
│       ├── dto/
│       │   ├── create-user.dto.ts
│       │   └── update-user.dto.ts
│       └── constants.ts
├── app.module.ts
├── main.ts
└── app.service.ts

2. Database Configuration


// src/config/database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const databaseConfig: TypeOrmModuleOptions = {
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: [__dirname + '/../**/*.entity.{ts,js}'],
  synchronize: process.env.NODE_ENV !== 'production', // Never true in production!
  logging: process.env.NODE_ENV === 'development',
  ssl: process.env.NODE_ENV === 'production',
};

3. Global Validation and Auth Guard


// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE, APP_GUARD } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { JwtAuthGuard } from './core/guards/jwt-auth.guard';
import { UsersModule } from './modules/users/users.module';
import { AuthModule } from './modules/auth/auth.module';

@Module({
  imports: [UsersModule, AuthModule],
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

4. JWT Authentication Guard


// src/core/guards/jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const authHeader = request.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) return false;
    const token = authHeader.split(' ')[1];
    try {
      const payload = this.jwtService.verify(token, { secret: process.env.JWT_SECRET });
      request.user = payload;
      return true;
    } catch (e) {
      return false;
    }
  }
}

5. Custom HTTP Exception Filter


// src/core/exceptions/http-exception.filter.ts
import { Catch, ExceptionFilter, HttpException, ArgumentsHost, Logger } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();
    const errorResponse = exception.getResponse();

    // Log the error for debugging
    this.logger.error(`HTTP Exception: ${exception.message}`, exception.stack);

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        error: typeof errorResponse === 'string' ? errorResponse : (errorResponse as any).message,
      });
  }
}

6. Auth Module with JWT


// src/modules/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '15m' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

7. Auth Controller and Service


// src/modules/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }
}

// src/modules/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { LoginDto } from './dto/login.dto';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async login(loginDto: LoginDto) {
    const user = await this.usersService.validateUser(loginDto.email, loginDto.password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    const payload = { sub: user.id, email: user.email };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

8. Users Module and Repository


// src/modules/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

// src/modules/users/users.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersRepository {
  constructor(
    @InjectRepository(User)
    private readonly repo: Repository,
  ) {}

  async findByEmail(email: string): Promise {
    return this.repo.findOne({ where: { email } });
  }

  async create(userData: Partial): Promise {
    const user = this.repo.create(userData);
    return this.repo.save(user);
  }
}

9. Users Controller


// src/modules/users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { JwtAuthGuard } from '../../core/guards/jwt-auth.guard';

@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

10. Users Service (Business Logic)


// src/modules/users/users.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(private repo: UsersRepository) {}

  async findAll() {
    return this.repo.findByEmail(''); // Returns all users (implementation may differ)
  }

  async create(createUserDto: CreateUserDto) {
    const existing = await this.repo.findByEmail(createUserDto.email);
    if (existing) {
      throw new ConflictException('Email already registered');
    }
    return this.repo.create(createUserDto);
  }

  async remove(id: string) {
    // repository method for deletion omitted for brevity
    return { deleted: true };
  }

  async validateUser(email: string, password: string) {
    // Verify password against hash; pseudocode
    const user = await this.repo.findByEmail(email);
    if (user && user.password === hash(password)) {
      // strip password before returning
      delete user.password;
      return user;
    }
    return null;
  }
}

Comparison Table

Aspect NestJS Express.js (plain) Fastify
Built‑in Dependency Injection Yes (core) No (needs extra libraries) Limited (needs extra)
TypeScript Support First‑class None (but available via TS‑ify) Good, but not native
Scalable Architecture Patterns Modules, interceptors, guards decent but fewer abstractions
Performance Good for most workloads, overhead minimal Fastest raw performance Very fast, optimized for speed
Out‑of‑the‑box Validation ValidationPipe + class‑validator Built‑in validation via schemas
Community & Ecosystem Strong, official CLI, plugins Massive (third‑party) Growing, solid core

Best Practices

  1. Keep Modules Small and Cohesive. Each module should have a clear, single responsibility. This reduces cyclic dependencies and makes testing easier.
  2. Use Providers with Proper Scope. Application‑wide providers should be stateless; request‑scoped providers are ideal for data like request id or user context.
  3. Separate Interfaces from Implementations. Use repository interfaces and abstract services to keep the code flexible.
  4. Apply Pipes for Input Validation. Global validation helps avoid many security issues and ensures consistent request shape.
  5. Leverage Interceptors for Cross‑Cutting Concerns. Logging, timing, caching, and transformation can all be added as reusable interceptors.
  6. Implement Guards to Protect Resources. Authentication, authorization, and rate limiting are enforced with guards.
  7. Structure Providers with Dependency Injection. Use constructor injection for services, and parameterize dependencies wherever possible.
  8. Write Unit Tests for Providers and Controllers. Nest's testing utilities make mocking providers straightforward.
  9. Follow SEMVER for API Versioning. Use route prefixes like /v1/users to version endpoints.
  10. Employ Environment‑Based Configuration. Separate configuration files for development, test, and production to avoid accidental leaks.

Common Mistakes

  • Circular Dependencies. Importing modules inside providers can cause loops. Use forward references or refactor into separate modules.
  • Over‑Injecting. Injecting many low‑value services can make the DI container heavy and obscure real dependencies.
  • Neglecting Input Validation. Trusting client‑provided data leads to bugs and security vulnerabilities.
  • Mixing HTTP and WebSocket Modules. While Nest supports both, mixing them in the same module can cause noisy code. Keep them separate.
  • Hard‑coding Secrets. Storing passwords, JWT secrets, or DB credentials in source code leads to exposure.
  • Ignoring Error Boundaries.