DEV Community

Abanoub Kerols
Abanoub Kerols

Posted on

Mastering Nest.js: From Core Concepts to a Real-World Project

What is Nest.js?

Nest.js is a progressive Node.js framework for building efficient, scalable, and maintainable back-end applications.
It’s built with TypeScript and heavily inspired by Angular’s modular architecture, which makes it a great choice for developers who value structure, testability, and clean code

Core Concepts and Features

1. Modules

A Nest.js application is made up of modules — the building blocks that organize code into reusable units.
Each module can contain controllers, services, and other providers

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

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

Enter fullscreen mode Exit fullscreen mode

2. Dependency Injection (DI)
Dependency Injection is one of Nest’s most powerful features.
It allows classes (like controllers or services) to automatically receive their dependencies without manually creating them

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

@Injectable()
export class UsersService {
  findAll() {
    return ['user1', 'user2', 'user3'];
  }
}

import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

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

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

Enter fullscreen mode Exit fullscreen mode

Here, UsersController automatically receives an instance of UsersService through DI.

3. Guards
Guards are used to control access to routes based on certain conditions, like authentication or user roles.
They decide whether a request will be processed or rejected.

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 === 'Bearer mytoken';
  }
}

Enter fullscreen mode Exit fullscreen mode

To apply it:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('profile')
export class ProfileController {
  @Get()
  @UseGuards(AuthGuard)
  getProfile() {
    return { name: 'John Doe', role: 'admin' };
  }
}

Enter fullscreen mode Exit fullscreen mode

4. Interceptors
Interceptors can transform requests or responses, handle logging, or measure performance before or after a route handler is executed.

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

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    const now = Date.now();
    return next.handle().pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
  }
}

Enter fullscreen mode Exit fullscreen mode

Apply it globally or to a single route:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('tasks')
@UseInterceptors(LoggingInterceptor)
export class TasksController {
  @Get()
  getTasks() {
    return ['Task 1', 'Task 2'];
  }
}

Enter fullscreen mode Exit fullscreen mode

5. Services
Services handle business logic and data manipulation.
They keep controllers clean and focused on handling HTTP requests

@Injectable()
export class TasksService {
  private tasks = ['Learn Nest.js', 'Build an API'];

  getAllTasks() {
    return this.tasks;
  }

  addTask(task: string) {
    this.tasks.push(task);
    return this.tasks;
  }
}

Enter fullscreen mode Exit fullscreen mode

Why Developers Choose Nest.js

  • TypeScript by default – better tooling and safety
  • Modular and testable architecture
  • Built-in DI, Guards, Interceptors, and Pipes
  • Great integration with TypeORM, GraphQL, and WebSockets
  • Perfect for enterprise-grade and scalable apps

Project Example: Simple To-Do API with Nest.js

Let’s build a simple To-Do API that allows users to view, add, and delete tasks.
We’ll use:

  • Service - to handle business logic
  • Controller - to handle HTTP routes
  • Guard - to protect routes with a fake token
  • Interceptor - to log requests and responses
  • Module - to organize everything

Project Structure

src/
 ├── app.module.ts
 ├── todos/
 │    ├── todos.module.ts
 │    ├── todos.controller.ts
 │    ├── todos.service.ts
 │    ├── auth.guard.ts
 │    └── logging.interceptor.ts
 └── main.ts
Enter fullscreen mode Exit fullscreen mode

1. todos.service.ts
Handles all the business logic and data.

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

@Injectable()
export class TodosService {
  private todos = [{ id: 1, title: 'Learn Nest.js' }];

  findAll() {
    return this.todos;
  }

  add(title: string) {
    const newTodo = { id: Date.now(), title };
    this.todos.push(newTodo);
    return newTodo;
  }

  delete(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
    return { message: 'Deleted successfully' };
  }
}
Enter fullscreen mode Exit fullscreen mode

2. todos.controller.ts
Defines the REST API endpoints.

import { Controller, Get, Post, Body, Delete, Param, UseGuards, UseInterceptors } from '@nestjs/common';
import { TodosService } from './todos.service';
import { AuthGuard } from './auth.guard';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('todos')
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
export class TodosController {
  constructor(private todosService: TodosService) {}

  @Get()
  getTodos() {
    return this.todosService.findAll();
  }

  @Post()
  addTodo(@Body('title') title: string) {
    return this.todosService.add(title);
  }

  @Delete(':id')
  removeTodo(@Param('id') id: number) {
    return this.todosService.delete(id);
  }
}

Enter fullscreen mode Exit fullscreen mode

3. auth.guard.ts
Blocks access unless the request has a valid token.

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const auth = req.headers.authorization;
    if (auth === 'Bearer mytoken') return true;
    throw new UnauthorizedException('Invalid or missing token');
  }
}
Enter fullscreen mode Exit fullscreen mode

4. logging.interceptor.ts
Logs how long each request takes.

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

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    console.log('Incoming request...');
    return next.handle().pipe(
      tap(() => console.log(`Response sent after ${Date.now() - now}ms`)),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

5. todos.module.ts
Brings everything together for this feature.

import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
import { AuthGuard } from './auth.guard';
import { LoggingInterceptor } from './logging.interceptor';

@Module({
  controllers: [TodosController],
  providers: [TodosService, AuthGuard, LoggingInterceptor],
})
export class TodosModule {}
Enter fullscreen mode Exit fullscreen mode

6. app.module.ts
The main application module.

import { Module } from '@nestjs/common';
import { TodosModule } from './todos/todos.module';

@Module({
  imports: [TodosModule],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

7. main.ts
Bootstrap the Nest.js application.

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  console.log('Server running on http://localhost:3000');
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Testing the API

Use Postman or curl:

Get all todos

GET http://localhost:3000/todos
Authorization: Bearer mytoken
Enter fullscreen mode Exit fullscreen mode

Add a todo

POST http://localhost:3000/todos
Authorization: Bearer mytoken
Body: { "title": "Build awesome projects" }
Enter fullscreen mode Exit fullscreen mode

Delete a todo

DELETE http://localhost:3000/todos/1
Authorization: Bearer mytoken
Enter fullscreen mode Exit fullscreen mode

Conclusion

This simple To-Do API demonstrates how Nest.js lets you write clean, modular, and testable code using modern features

  • Services handle business logic
  • Guards secure endpoints
  • Interceptors handle cross-cutting concerns
  • Dependency Injection makes everything easily testable

Nest.js turns back-end development into a structured, elegant, and maintainable experience.

Top comments (0)