Изучаем NestJS: примеры и инструкции для серверной разработки

Раздел: Программирование -> Серверная разработка

Основной подход: создание REST API с помощью CLI, TypeORM и валидации

Как быстро развернуть REST API с валидацией данных на NestJS?

NestJS предоставляет мощный CLI, который генерирует каркас приложения. Для начала установим пакет и создадим проект:

npm i -g @nestjs/cli
nest new my-project --package-manager npm

Nest js (nestjs фреймворк)

Выбираем менеджер пакетов и следуем инструкциям. Для работы с базой данных установим TypeORM и модуль PostgreSQL:

npm install @nestjs/typeorm typeorm pg

серверы js (серверы на javascript)

В корневом модуле (app.module.ts) подключаем TypeORM:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'user',
      password: 'pass',
      database: 'test',
      autoLoadEntities: true,
      synchronize: true, // только для разработки
    }),
    UsersModule,
  ],
})
export class AppModule {}

Создадим модуль Users с CRUD-операциями. Используем команду CLI:

nest g resource users

Выбираем REST API и генерируем CRUD-методы. В результате получаем файлы контроллера, сервиса, DTO и сущности. Для валидации устанавливаем class-validator и class-transformer:

npm install class-validator class-transformer

Добавим валидацию в DTO (create-user.dto.ts):

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

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;
}

В контроллере используем ValidationPipe глобально или локально. Подключим глобальный пайп в main.ts:

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

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

Теперь все входящие запросы проходят валидацию. Пример ошибки валидации (ответ 400):

{
  "statusCode": 400,
  "message": ["name must be longer than or equal to 2 characters"],
  "error": "Bad Request"
}

Цель и случай использования: этот подход подходит для большинства проектов, где нужна стандартная REST-архитектура с быстрой разработкой и валидацией.

Типичная ошибка: synchronize: true в продакшене приводит к потере данных. В production следует использовать миграции. Решение: создать миграцию через TypeORM CLI.

npx typeorm migration:create -n InitialMigration
npx typeorm migration:run

Как заменить TypeORM на Prisma для работы с базой данных?

Prisma предлагает более строгую типизацию и простую настройку. Установим Prisma CLI и клиент:

npm install prisma @prisma/client
npx prisma init

Создаём модель User в schema.prisma:

model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
  createdAt DateTime @default(now())
}

Выполняем миграцию:

npx prisma migrate dev --name init

Генерируем Prisma Client:

npx prisma generate

Создаём модуль PrismaService для внедрения зависимостей:

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

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }
  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Подключаем PrismaService в модуль Users и используем в сервисе:

import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { CreateUserDto } from './dto/create-user.dto';

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

  async create(createUserDto: CreateUserDto) {
    return this.prisma.user.create({ data: createUserDto });
  }
}

Цель: Prisma удобна для проектов, где важна строгая типизация и автоматическая генерация клиента. Подходит для быстрой разработки и небольших команд.

Проблема: при изменении схемы Prisma необходимо перегенерировать клиент. Забыли выполнить prisma generate - получите ошибки типов. Решение: добавить скрипт в package.json для автоматической генерации после миграции.

Как использовать Fastify вместо Express в NestJS?

NestJS по умолчанию работает с Express. Для повышения производительности можно переключиться на Fastify. Установим платформу Fastify:

npm install @nestjs/platform-fastify fastify

Изменим main.ts для использования Fastify:

import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

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

Обратите внимание: Fastify не поддерживает некоторые middleware Express (например, cors) напрямую. Вместо этого используйте встроенные методы Fastify или пакеты @fastify/cors:

npm install @fastify/cors

Подключение CORS:

import cors from '@fastify/cors';

async function bootstrap() {
  const app = await NestFactory.create(
    AppModule,
    new FastifyAdapter()
  );
  await app.register(cors, { origin: '*' });
  await app.listen(3000);
}

Цель: Fastify даёт прирост производительности (до 2-3 раз) за счёт асинхронной обработки запросов. Подходит для высоконагруженных проектов или микросервисов.

Ошибка: использование Express-специфичных пакетов (например, multer) с Fastify вызовет проблемы. Решение: перейти на аналоги из @fastify/* или использовать универсальные решения.

Как реализовать API через GraphQL вместо REST?

NestJS поддерживает GraphQL через модуль @nestjs/graphql. Установим пакеты:

npm install @nestjs/graphql @nestjs/apollo graphql apollo-server-express

В корневом модуле подключаем GraphQL:

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      driver: ApolloDriver,
      autoSchemaFile: 'schema.gql',
    }),
  ],
})
export class AppModule {}

Создадим резолвер для User:

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Resolver('User')
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query(() => [User])
  async users() {
    return this.usersService.findAll();
  }

  @Mutation(() => User)
  async createUser(@Args('createUserInput') createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}

Цель: GraphQL даёт клиенту возможность запрашивать только нужные поля. Подходит для сложных приложений с множеством связей (например, социальные сети, панели управления).

Сложность: для валидации аргументов в GraphQL используйте class-validator с декоратором @InputType(). Типичная ошибка - забыть добавить @InputType() для DTO, что вызовет ошибку схемы.

Расширенные примеры кода для NestJS

Ниже приведены углублённые примеры, которые помогут освоить более редкие сценарии.

1. Пример создания полноценного CRUD с пагинацией и фильтрацией

Сервис Users с методами findAll с пагинацией (page, limit) и фильтром по имени:

Пример
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository,
  ) {}

  async findAll(page: number = 1, limit: number = 10, name?: string) {
    const query = this.usersRepository.createQueryBuilder('user');
    if (name) {
      query.where('user.name LIKE :name', { name: `%${name}%` });
    }
    const total = await query.getCount();
    const users = await query.skip((page - 1) * limit).take(limit).getMany();
    return { data: users, total, page, limit };
  }

  async create(createUserDto: CreateUserDto) {
    const user = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(user);
  }
}

Запрос через curl:

curl "http://localhost:3000/users?page=1&limit=5&name=john"

Ответ:
{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com",
      "createdAt": "2025-03-01T12:00:00.000Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 5
}

2. Использование декоратора @UseInterceptors для логирования

Создадим интерсептор, который логирует время выполнения запроса:

Пример
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 {
    const now = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`Request took ${Date.now() - now}ms`)),
    );
  }
}

Применяем интерсептор к контроллеру:

Пример
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  @Get()
  findAll() {
    return [];
  }
}

Результат в консоли сервера:

Request took 12ms

3. Настройка глобального фильтра исключений

Создадим фильтр, который форматирует ошибки в едином формате:

Пример
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { 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 status = exception.getStatus();
    const message = exception.getResponse();

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

Подключаем глобально в main.ts:

Пример
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

Результат (например, при обращении к несуществующему роуту):

{
  "statusCode": 404,
  "timestamp": "2025-03-01T14:30:00.000Z",
  "message": "Cannot GET /nonexistent"
}

4. Пример миграции TypeORM с помощью CLI

Настроим синхронизацию через миграции. Сначала изменяем app.module.ts, убираем synchronize.

Пример
TypeOrmModule.forRoot({
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'user',
  password: 'pass',
  database: 'test',
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  migrations: [__dirname + '/migrations/*{.ts,.js}'],
  cli: {
    migrationsDir: 'src/migrations',
  },
})

Создаём миграцию:

Пример
npx ts-node node_modules/.bin/typeorm migration:create -n AddUserAge

Редактируем миграционный файл:

Пример
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

export class AddUserAge1234567890 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.addColumn('user', new TableColumn({
      name: 'age',
      type: 'int',
      isNullable: true,
    }));
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropColumn('user', 'age');
  }
}

Запускаем миграцию:

Пример
npx ts-node node_modules/.bin/typeorm migration:run

Результат в консоли:

Migration AddUserAge1234567890 has been executed successfully.

NestJS фреймворк - comments

En
Nest js (javascript)