Простейшая архитектура веб-приложения часто выглядит так: есть сервер, который отдает готовые HTML-страницы, и есть дополнительные сервисы, например база данных, очередь сообщений, внешние API и файловое хранилище
Такой подход работает, но со временем у него появляются ограничения:
Но можно внести ряд улучшений:
В итоге получаем, что в каждом сервисе нет зависимости от языка и от фреймворка, а бэкенд-разработчик не взаимодействует с сервисами, которые ему не требуются
Так появляется архитектурный паттерн “бэкенд для фронтенда” - в нем для каждого уникального фронтенда есть свой бэкенд. Этот бэкенд будет заниматься:
Серверный фреймворк Nest для Node.js отлично подходит для этой задачи. Его архитектура включает:
Также Nest концептуально похож на Angular:
Архитектура Nest строится вокруг модулей. Модуль объединяет связанные части приложения:
Контроллер принимает HTTP-запрос и возвращает ответ. В нем не стоит хранить сложную бизнес-логику: обычно он лишь принимает параметры, вызывает сервис и возвращает результат.
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
}
Здесь:
@Controller('users') задает базовый путь@Get(':id') связывает метод с GET /users/:idParseIntPipe преобразует параметр строки в число и выбросит ошибку, если преобразование невозможноСервис обычно содержит прикладную или бизнес-логику:
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
findOne(id: number) {
const user = this.users.find((item) => item.id === id);
if (!user) {
throw new NotFoundException('Пользователь не найден');
}
return user;
}
}
@Injectable() означает, что класс можно зарегистрировать в контейнере зависимостей и внедрять в другие классы
Модуль описывает, какие контроллеры и провайдеры относятся к одной области приложения.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Запуск HTTP-приложения обычно происходит в main.ts:
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();
Глобальный ValidationPipe здесь:
whitelist)transform)Одна из ключевых идей Nest - внедрение зависимостей. Если сервису нужен другой сервис, его можно не создать вручную, а получить от контейнера:
constructor(private readonly usersService: UsersService) {}
Плюсы такого подхода:
В Nest зависимости обычно называются провайдерами
Для описания входных данных обычно используют DTO (Data Transfer Object)
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(2)
name: string;
}
Использование в контроллере:
import { Body, Controller, Post } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
return dto;
}
}
Валидация в Nest обычно строится так:
class-validator описывают правилаValidationPipe запускает проверку и преобразованиеПайпы в Nest - это специальные классы, которые могут:
Примеры встроенных pipe: ValidationPipe, ParseIntPipe, ParseBoolPipe, ParseUUIDPipe
Охраняющие декораторы (или гуарды, от guard) проверяют, можно ли вообще выполнять обработчик запроса
Типичный пример - это проверка JWT (JSON Web Token) для авторизации и ролей пользователя
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return Boolean(request.headers.authorization);
}
}
Использование:
@UseGuards(AuthGuard)
@Get('profile')
getProfile() {
return { ok: true };
}
Интерцептор оборачивает вызов обработчика. Он может:
Observableimport {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next.handle().pipe(
map((data) => ({
data,
timestamp: new Date().toISOString(),
})),
);
}
}
Фильтры перехватывают исключения и преобразуют их в HTTP-ответ:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
});
}
}
Это полезно, например, для создания шаблонных страниц с ошибками, такими как HTTP 404
Промежуточное ПО (Middleware) выполняется раньше, чем гуарды и контроллер. Он удобен для:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
}
Nest хорошо интегрируется с ORM и драйверами баз данных. Часто используют библиотеки:
Для этого реализуют репозиторий - абстракцию доступа к данным. Репозиторий скрывает детали хранения и предоставляет понятный интерфейс для предметной области. Например:
interface UsersRepository {
findById(id: number): Promise<User | null>;
save(user: User): Promise<void>;
}
При работе с базой данных часто различают два подхода:
Nest поддерживает оба подхода через выбранный инструмент доступа к данным
Кэширование в бэкенде для фронтенда особенно полезно, если один и тот же клиент часто запрашивает одинаковые агрегированные данные. В Nest кэширование можно подключать через менеджер кэширования, а в качестве внешнего хранилища часто используют Redis
Redis полезен, когда нужно:
Для документирования HTTP API в Nest часто используют Swagger-модуль, который строит OpenAPI-описание
Пример настройки:
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Users API')
.setDescription('Документация сервиса пользователей')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
Для описания DTO используют декораторы:
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'user@example.com' })
email: string;
}
В Nest можно создавать свои декораторы, чтобы повторно использовать типичную логику. Например, чтобы получать текущего пользователя из запроса:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
Использование:
@Get('me')
getMe(@CurrentUser() user: User) {
return user;
}
На практике Nest часто используется как API-слой для фронтенда. Он может:
Для фронтенда это удобно, потому что: