Изучаем NestJS: примеры и инструкции для серверной разработки
Основной подход: создание REST API с помощью CLI, TypeORM и валидации
Как быстро развернуть REST API с валидацией данных на NestJS?
NestJS предоставляет мощный CLI, который генерирует каркас приложения. Для начала установим пакет и создадим проект:
npm i -g @nestjs/cli
nest new my-project --package-manager npmNest 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.