NestJS: Створення повноцінного REST API

Повний посібник зі створення професійного REST API з NestJS. Контролери, сервіси, модулі, валідація з class-validator та обробка помилок з практичними прикладами.

Посібник з NestJS для створення повноцінного REST API

NestJS утвердився як провідний Node.js фреймворк для створення масштабованих серверних застосунків, які легко підтримувати. Натхненний Angular, він пропонує модульну архітектуру, впровадження залежностей та нативну підтримку TypeScript. Розробка професійних REST API стає структурованою та передбачуваною.

Чому NestJS у 2026 році

NestJS 11 вводить значні покращення продуктивності, нативну підтримку сигналів для graceful shutdown та спрощену інтеграцію з сучасними ORM, такими як Prisma та Drizzle. Фреймворк зберігає повну зворотну сумісність з попередніми версіями.

Встановлення та налаштування проєкту

CLI NestJS генерує повноцінний проєкт із готовою до продакшену структурою. TypeScript, ESLint та модульні тести налаштовуються автоматично.

bash
# terminal
# Global installation of the NestJS CLI
npm install -g @nestjs/cli

# Create a new project
nest new my-api

# Navigate to the project
cd my-api

# Start in development mode with hot-reload
npm run start:dev

Ця команда створює проєкт з організованою файловою структурою та попередньо встановленими необхідними залежностями.

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

async function bootstrap() {
  // Create the NestJS application
  const app = await NestFactory.create(AppModule);

  // Enable global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,           // Removes non-decorated properties
    forbidNonWhitelisted: true, // Rejects requests with unknown properties
    transform: true,            // Transforms payloads to DTO instances
  }));

  // Configure global prefix for all routes
  app.setGlobalPrefix('api');

  await app.listen(3000);
}
bootstrap();

Ця конфігурація вмикає автоматичну валідацію запитів та додає префікс /api до всіх маршрутів.

Розуміння модульної архітектури

NestJS організовує код у модулі, кожен з яких інкапсулює функціональну область. Модулі оголошують контролери (HTTP-ендпоінти), провайдери (бізнес-сервіси) та імпорти (залежності).

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';
import { AuthModule } from './auth/auth.module';

// The root module imports all application modules
@Module({
  imports: [
    UsersModule,    // User management
    ProductsModule, // Product catalog
    AuthModule,     // Authentication
  ],
})
export class AppModule {}

Кожен бізнес-модуль має однакову структуру: файл модуля, контролер та сервіс.

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

@Module({
  // Controllers handle HTTP requests
  controllers: [UsersController],
  // Providers are injectable throughout the module
  providers: [UsersService],
  // Exports make providers available to other modules
  exports: [UsersService],
})
export class UsersModule {}

Експортування UsersService дозволяє іншим модулям імпортувати UsersModule та використовувати цей сервіс через впровадження залежностей.

Створення повноцінного CRUD-контролера

Контролери визначають HTTP-маршрути та делегують бізнес-логіку сервісам. Декоратори NestJS роблять код виразним та самодокументованим.

src/users/users.controller.tstypescript
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  HttpCode,
  HttpStatus,
  ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

// Route prefix: /api/users
@Controller('users')
export class UsersController {
  // Inject service via constructor
  constructor(private readonly usersService: UsersService) {}

  // POST /api/users - Create a user
  @Post()
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
    // The DTO is automatically validated before reaching here
    return this.usersService.create(createUserDto);
  }

  // GET /api/users - List with pagination
  @Get()
  async findAll(
    @Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
    @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10,
  ): Promise<{ data: User[]; total: number }> {
    return this.usersService.findAll(page, limit);
  }

  // GET /api/users/:id - Retrieve by ID
  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
    // ParseIntPipe automatically converts and validates the parameter
    return this.usersService.findOne(id);
  }

  // PUT /api/users/:id - Full update
  @Put(':id')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<User> {
    return this.usersService.update(id, updateUserDto);
  }

  // DELETE /api/users/:id - Deletion
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
    await this.usersService.remove(id);
  }
}

Pipes, такі як ParseIntPipe, забезпечують конвертацію та валідацію параметрів. У разі невдачі автоматично повертається помилка 400.

Вбудовані валідаційні Pipes

NestJS надає кілька вбудованих pipes: ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe. Кожен з них валідує та трансформує вхідні дані перед виконанням обробника.

Реалізація бізнес-сервісу

Сервіси інкапсулюють бізнес-логіку та доступ до даних. Позначені декоратором @Injectable(), вони керуються контейнером впровадження залежностей NestJS.

src/users/users.service.tstypescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  // In-memory database simulation
  private users: User[] = [];
  private idCounter = 1;

  async create(createUserDto: CreateUserDto): Promise<User> {
    // Create the user entity
    const user: User = {
      id: this.idCounter++,
      ...createUserDto,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    this.users.push(user);
    return user;
  }

  async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
    // Calculate pagination
    const start = (page - 1) * limit;
    const end = start + limit;

    return {
      data: this.users.slice(start, end),
      total: this.users.length,
    };
  }

  async findOne(id: number): Promise<User> {
    const user = this.users.find(u => u.id === id);

    // Throw exception if user doesn't exist
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user;
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id);

    // Merge existing data with updates
    Object.assign(user, updateUserDto, { updatedAt: new Date() });

    return user;
  }

  async remove(id: number): Promise<void> {
    const index = this.users.findIndex(u => u.id === id);

    if (index === -1) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    this.users.splice(index, 1);
  }

  // Utility method for other services
  async findByEmail(email: string): Promise<User | undefined> {
    return this.users.find(u => u.email === email);
  }
}

Виняток NotFoundException автоматично генерує HTTP 404 відповідь з форматованим повідомленням про помилку.

Готовий до співбесід з Node.js / NestJS?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Валідація даних з class-validator

DTO (Data Transfer Objects) визначають структуру вхідних даних. Декоратори з class-validator задають правила валідації, які застосовуються автоматично.

src/users/dto/create-user.dto.tstypescript
import {
  IsEmail,
  IsNotEmpty,
  IsString,
  MinLength,
  MaxLength,
  IsOptional,
  Matches,
} from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty({ message: 'Email is required' })
  @IsEmail({}, { message: 'Invalid email format' })
  email: string;

  @IsNotEmpty({ message: 'Password is required' })
  @MinLength(8, { message: 'Password must be at least 8 characters' })
  @MaxLength(50, { message: 'Password cannot exceed 50 characters' })
  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
    { message: 'Password must contain at least one uppercase, one lowercase, and one number' }
  )
  password: string;

  @IsNotEmpty({ message: 'First name is required' })
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  firstName: string;

  @IsNotEmpty({ message: 'Last name is required' })
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  lastName: string;

  @IsOptional()
  @IsString()
  @MaxLength(20)
  phone?: string;
}

Для часткових оновлень PartialType робить всі поля необов'язковими, зберігаючи при цьому правила валідації.

src/users/dto/update-user.dto.tstypescript
import { PartialType, OmitType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// All fields from CreateUserDto become optional
// Password is excluded from standard updates
export class UpdateUserDto extends PartialType(
  OmitType(CreateUserDto, ['password'] as const)
) {}

Сутність представляє структуру збережених даних.

src/users/entities/user.entity.tstypescript
export class User {
  id: number;
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  phone?: string;
  createdAt: Date;
  updatedAt: Date;
}

Централізована обробка помилок

NestJS надає вбудовані HTTP-винятки. Глобальний фільтр винятків дозволяє налаштовувати формати відповідей про помилки.

src/common/filters/http-exception.filter.tstypescript
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

// Catches all HttpExceptions
@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();

    // Retrieve error message (can be string or object)
    const exceptionResponse = exception.getResponse();
    const message = typeof exceptionResponse === 'string'
      ? exceptionResponse
      : (exceptionResponse as any).message;

    // Standardized response format
    response.status(status).json({
      success: false,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: message,
    });
  }
}

Для перехоплення помилок, що не є HTTP (системні помилки, необроблені винятки), другий фільтр забезпечує повне покриття.

src/common/filters/all-exceptions.filter.tstypescript
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

// Catches ALL exceptions (including system errors)
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // Determine HTTP code and message
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = exception instanceof HttpException
      ? exception.message
      : 'Internal server error';

    // Log error for debugging
    console.error('Exception caught:', exception);

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

Глобальна реєстрація фільтрів у головному модулі.

src/main.ts (updated)typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';

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

  // Global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));

  // Global exception filters
  app.useGlobalFilters(
    new AllExceptionsFilter(),
    new HttpExceptionFilter(),
  );

  app.setGlobalPrefix('api');
  await app.listen(3000);
}
bootstrap();
Порядок фільтрів має значення

Порядок реєстрації фільтрів є важливим. Перший зареєстрований фільтр виконується останнім. AllExceptionsFilter повинен бути зареєстрований перед HttpExceptionFilter, щоб виконувати роль запасного механізму.

Інтеграція з Prisma ORM

Prisma спрощує взаємодію з базою даних завдяки автоматично згенерованому типізованому клієнту. Нижче представлена повна інтеграція з NestJS.

bash
# terminal
# Install Prisma
npm install prisma @prisma/client

# Initialize Prisma with PostgreSQL
npx prisma init --datasource-provider postgresql

Визначення схеми даних.

prisma/schema.prismaprisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  firstName String   @map("first_name")
  lastName  String   @map("last_name")
  phone     String?
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  // Relations
  posts     Post[]

  @@map("users")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  authorId  Int      @map("author_id")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  // Relation to User
  author    User     @relation(fields: [authorId], references: [id])

  @@map("posts")
}

Створення багаторазового модуля Prisma.

src/prisma/prisma.service.tstypescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  // Automatic connection on module startup
  async onModuleInit() {
    await this.$connect();
  }

  // Clean disconnection on application shutdown
  async onModuleDestroy() {
    await this.$disconnect();
  }
}
src/prisma/prisma.module.tstypescript
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

// @Global makes the service available throughout the application
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Оновлений сервіс Users з використанням Prisma.

src/users/users.service.ts (Prisma version)typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from '@prisma/client';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  async create(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
    // Check email uniqueness
    const existingUser = await this.prisma.user.findUnique({
      where: { email: createUserDto.email },
    });

    if (existingUser) {
      throw new ConflictException('This email is already in use');
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    // Create the user
    const user = await this.prisma.user.create({
      data: {
        ...createUserDto,
        password: hashedPassword,
      },
    });

    // Exclude password from response
    const { password, ...result } = user;
    return result;
  }

  async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
    // Parallel execution of count and paginated query
    const [data, total] = await Promise.all([
      this.prisma.user.findMany({
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { createdAt: 'desc' },
        select: {
          id: true,
          email: true,
          firstName: true,
          lastName: true,
          phone: true,
          createdAt: true,
          updatedAt: true,
        },
      }),
      this.prisma.user.count(),
    ]);

    return { data: data as User[], total };
  }

  async findOne(id: number): Promise<Omit<User, 'password'>> {
    const user = await this.prisma.user.findUnique({
      where: { id },
      select: {
        id: true,
        email: true,
        firstName: true,
        lastName: true,
        phone: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user as Omit<User, 'password'>;
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
    // Check existence
    await this.findOne(id);

    const user = await this.prisma.user.update({
      where: { id },
      data: updateUserDto,
      select: {
        id: true,
        email: true,
        firstName: true,
        lastName: true,
        phone: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    return user as Omit<User, 'password'>;
  }

  async remove(id: number): Promise<void> {
    await this.findOne(id);
    await this.prisma.user.delete({ where: { id } });
  }
}

Інтерсептори для трансформації відповідей

Інтерсептори дозволяють одноманітно перетворювати відповіді. Інтерсептор трансформації стандартизує формат усіх відповідей API.

src/common/interceptors/transform.interceptor.tstypescript
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// Interface for standardized response format
export interface ApiResponse<T> {
  success: boolean;
  data: T;
  timestamp: string;
}

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

Інтерсептор логування відстежує запити та час їх виконання.

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

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const now = Date.now();

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const { statusCode } = response;
        const duration = Date.now() - now;

        // Structured log format
        this.logger.log(
          `${method} ${url} ${statusCode} - ${duration}ms`
        );
      }),
    );
  }
}

Висновок

NestJS забезпечує надійну та масштабовану архітектуру для створення професійних REST API. Поєднання TypeScript, впровадження залежностей та виразних декораторів дозволяє створювати застосунки, які легко підтримувати та тестувати.

Чек-лист для якісного NestJS API

  • Модульна структура з розділенням відповідальності
  • DTO з валідацією class-validator для всіх вхідних даних
  • Спеціалізовані сервіси для бізнес-логіки
  • Централізована обробка помилок з фільтрами винятків
  • Інтерсептори для трансформації та логування
  • Інтеграція Prisma для доступу до бази даних
  • Глобальний ValidationPipe з увімкненим whitelist
  • Єдиний префікс API на всіх маршрутах

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Сила NestJS полягає в його структурованій архітектурі, яка природно спрямовує до найкращих практик. Перевірені патерни, такі як впровадження залежностей та пошарове розділення, створюють код, який легко тестувати та розвивати, готовий для корпоративних застосунків.

Теги

#nestjs
#nodejs
#typescript
#rest api
#backend

Поділитися

Пов'язані статті