Skip to content
All essays
WebMarch 27, 202615 min

NestJS Basics: A Progressive Node.js Framework

Learn the fundamentals of NestJS, a powerful framework for building efficient and scalable Node.js server-side applications with TypeScript

Ü
Ümit Uz
Mobile & Full Stack Developer

NestJS has emerged as one of the most popular Node.js frameworks for building server-side applications. Combining the best practices from object-oriented programming, functional programming, and reactive programming, NestJS provides a robust architecture for building scalable and maintainable applications. Built with TypeScript from the ground up, it offers excellent developer experience with strong typing and modern JavaScript features.

Getting Started with NestJS

NestJS is built around several key concepts that make it unique in the Node.js ecosystem. At its core, NestJS uses Express by default but also supports other adapters like Fastify. The framework provides a dependency injection system, modular architecture, and a rich set of tools for building everything from simple REST APIs to complex microservices.

Installation and Setup

Getting started with NestJS is straightforward. You can create a new project using the NestJS CLI:

bash
# Install NestJS CLI globally
npm install -g @nestjs/cli

# Create a new project
nest new my-nestjs-app

# Navigate to the project
cd my-nestjs-app

# Start the development server
npm run start:dev

This creates a structured project with all the necessary configurations and a basic application skeleton. The CLI generates several important directories and files that form the foundation of your NestJS application.

Project Structure

A typical NestJS project follows a clear and organized structure:

typescript
src/
├── app.controller.ts
├── app.controller.spec.ts
├── app.module.ts
├── app.service.ts
├── main.ts
└── main.hmr.ts (for hot module replacement)

// main.ts - Application entry point
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

The main.ts file is the entry point where we create a NestJS application instance using the NestFactory. This bootstraps the application and starts listening on a specified port.

Core Concepts

NestJS introduces several core concepts that every developer should understand.

Modules

Modules are the basic building blocks of a NestJS application. Each application has at least one module, the root module. Modules help organize the application into cohesive blocks of functionality:

typescript
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Modules use decorators to define their metadata. The @Module decorator takes an object that specifies controllers, providers, and exports. This modular approach makes the application more maintainable and testable.

Controllers

Controllers are responsible for handling incoming requests and returning responses to the client. They define routes and handle HTTP requests:

typescript
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

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

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

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

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

Controllers use dependency injection to access services. The @Controller decorator specifies the route prefix, while method decorators like @Get, @Post, @Put, and @Delete define the HTTP methods.

Providers

Providers are services that contain business logic and can be injected into controllers and other providers. They are decorated with @Injectable:

typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  private users = [];

  findAll() {
    return this.users;
  }

  findOne(id: string) {
    return this.users.find(user => user.id === id);
  }

  create(userData: any) {
    const user = {
      id: Date.now().toString(),
      ...userData,
    };
    this.users.push(user);
    return user;
  }
}

Providers encapsulate complex business logic and promote code reusability. They can be injected wherever needed using constructor injection.

Dependency Injection

NestJS features a powerful dependency injection (DI) system that manages the dependencies between different components. The DI container automatically resolves and injects dependencies:

typescript
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class OrderService {
  constructor(
    private usersService: UsersService,
    private productsService: ProductsService,
  ) {}

  createOrder(orderData: any) {
    // Access injected services
    const user = this.usersService.findOne(orderData.userId);
    const product = this.productsService.findOne(orderData.productId);
    // Process order logic
    return { user, product };
  }
}

This inversion of control makes the code more testable and maintainable. You can easily mock dependencies in tests and swap implementations without modifying the consuming code.

DTOs and Validation

Data Transfer Objects (DTOs) define the shape of data that moves through the application. Combined with validation pipes, they ensure data integrity:

typescript
import { IsString, IsEmail, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  username: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(6)
  password: string;
}

NestJS integrates seamlessly with class-validator for automatic validation. You can enable validation globally or on specific routes using pipes.

Middleware and Pipes

NestJS provides middleware and pipes for preprocessing requests and transforming data:

typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`Request: ${req.method} ${req.url}`);
    next();
  }
}

// Custom pipe for data transformation
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value === 'string') {
      return value.trim();
    }
    return value;
  }
}

Middleware functions have access to the request and response objects and can modify them. Pipes transform input data and perform validation.

Exception Filters

NestJS provides a built-in exception layer that handles errors gracefully. You can create custom exception filters for specific error handling:

typescript
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: exception.message,
      });
  }
}

Exception filters give you fine-grained control over error responses and can log errors for monitoring purposes.

Guards and Interceptors

Guards determine whether a request should be handled by the route handler, while interceptors transform the response or add extra logic:

typescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return !!request.headers.authorization;
  }
}

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
      }))
    );
  }
}

Guards are perfect for authentication and authorization, while interceptors can modify responses, add logging, or implement caching.

Configuration Management

NestJS provides a robust configuration module for managing environment-specific settings:

typescript
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
  ],
})
export class AppModule {
  constructor(private configService: ConfigService) {
    const databaseUrl = this.configService.get<string>('DATABASE_URL');
    const port = this.configService.get<number>('PORT', 3000);
  }
}

This approach keeps configuration separate from code and makes it easy to manage different environments.

Testing NestJS Applications

NestJS makes testing straightforward with utilities for unit and e2e testing:

typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('UsersController', () => {
  let controller: UsersController;
  let service: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [
        {
          provide: UsersService,
          useValue: {
            findAll: jest.fn().mockReturnValue([]),
            findOne: jest.fn().mockReturnValue({}),
            create: jest.fn().mockReturnValue({}),
          },
        },
      ],
    }).compile();

    controller = module.get<UsersController>(UsersController);
    service = module.get<UsersService>(UsersService);
  });

  it('should return an array of users', () => {
    expect(controller.findAll()).toEqual([]);
  });
});

NestJS provides utilities for mocking dependencies and creating test modules that mirror the production setup.

Conclusion

NestJS provides a comprehensive framework for building server-side applications with Node.js and TypeScript. Its modular architecture, dependency injection system, and extensive tooling make it an excellent choice for projects of any size. By following NestJS conventions and best practices, you can build applications that are scalable, maintainable, and testable.

The framework's opinionated structure helps teams organize code consistently, while its flexibility allows you to adapt it to various use cases. Whether you're building REST APIs, GraphQL servers, or microservices, NestJS provides the tools and patterns needed to succeed.

As you continue your NestJS journey, explore its advanced features like microservices support, web sockets, GraphQL integration, and the thriving ecosystem of third-party packages. The official documentation and community resources provide excellent guidance for taking your NestJS skills to the next level.

Enjoyed this essay? Share it.

Related essays

Next essay
Building GraphQL APIs with NestJS