![](https://habrastorage.org/getpro/habr/upload_files/0a3/5e0/cd2/0a35e0cd26039f9ab791701a9a97aa78.jpeg)
В экосистеме Node.js существует довольно много библиотек и фреймворков, которые пользуются определенной популярностью в сообществе. Но ни один из инструментов не решил главную проблему, с которой сталкиваются разработчики, когда пытаются писать бэкенд на Node.js. Это проблема выбора архитектуры.
Хочу обратить ваше внимание на относительно молодой фреймворк Nest.js. Из коробки он предлагает заранее предопределенную архитектуру, которая заточена под максимально удобную поддержку и масштабируемость вашего приложения. Заложенные архитектурные подходы проверены временем и давно используются в других, более зрелых платформах: Java(Spring), Python(Django), PHP(Laravel) и прочих.
Авторы Nest.js не скрывают, что их вдохновил один из популярных фреймворков для клиентских приложений — Angular.js, а его авторы ориентировались на походы, используемые в Java и C#. Если вы знакомы с Angular.js, то увидите в Nest.js много схожих идей.
Основные концепции
Разберем основные концепции, на которых строится разработка Nest.js-приложений.
Модули
Базовым строительным блоком приложения является модуль. Для его определения существует специальный декоратор @Module()
. Модуль содержит в себе какую-либо логику, которая удовлетворяет принципу единой ответственности. Например, это может быть модуль авторизации пользователей, модуль работы с товарами или модуль пользовательских рассылок.
![](https://habrastorage.org/getpro/habr/upload_files/123/826/6ee/1238266eecf9b898f8a10b8b5fb8a551.png)
Структура модулей приложения представляется в виде графа, согласно которому любое Nest.js-приложение должно иметь в cвоей основе так называемый корневой модуль. В нем группируются остальные модули. Внутри себя каждый модуль организует работу других строительных блоков приложения: контроллера и провайдеров. Немного позже мы разберем их подробно.
Ниже приведен пример определения модуля.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Контроллеры
Другой тип строительных блоков — это контроллеры. Они отвечают за обработку входящих запросов и возврат ответа клиенту. Контроллеры определяются с помощью декоратора @Controller()
. В качестве аргумента в него передается строка, которая назначает корневой путь для входящих запросов. Так как каждый контроллер принадлежит конкретному модулю, он автоматически будет удовлетворять принципу единой ответственности. То есть каждый контроллер сосредоточен на запросах клиента для одной конкретной функциональности.
![](https://habrastorage.org/getpro/habr/upload_files/d1c/a28/5a5/d1ca285a54a6f22f013173a87465c890.png)
Давайте рассмотрим наглядный пример определения контроллера по работе с пользователями.
import { Controller, Get } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get('/:id')
getUserById(@Param('id') id: number) {
return 'return user by id';
}
@Post()
create(@Body() dto: CreateUserDto) {
return 'new user created';
}
}
Nest.js содержит и другие декораторы запросов, с помощью которых можно реализовать API в стиле CRUD или REST: @Get, @Post, @Put, @Delete
.
Провайдеры (Сервисы)
Провайдеры предназначены для инкапсуляции логики самого приложения. Это может быть как бизнес-логика, так и инфраструктурная логика. На уровне провайдеров используется один из популярных и привычных нам паттернов — внедрение зависимостей. Таким образом мы можем внедрять провайдеры в контроллеры или другие высокоуровневые провайдеры. Для определения провайдера используется специальный декоратор @Injectable()
.
Также стоит упомянуть, что провайдеры иногда называют сервисами по примеру того, как эти элементы именуются в других фреймворках.
![](https://habrastorage.org/getpro/habr/upload_files/aec/d4d/177/aecd4d17757f19711099dfdc177eb22c.png)
Давайте рассмотрим пример определения провайдера и встраивания его в контроллер.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { PostModel } from './posts.model';
import { CreatePostDto } from './dto/create-post.dto';
@Injectable()
export class PostsProvider {
constructor(@InjectModel(PostModel) private postRepo: typeof PostModel) {
}
async create(dto: CreatePostDto) {
return await this.postRepo.create(dto);
}
async findAll() {
return await this.postRepo.findAll();
}
async findById(id: number) {
return await this.postRepo.findOne({ where: { id } });
}
}
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { PostsProvider } from './posts.providers';
import { CreatePostDto } from './dto/create-post.dto';
import { PostModel } from './posts.model';
@Controller('posts')
export class PostsController {
constructor(private postsProvider: PostsProvider) {
}
@Post()
create(@Body() dto: CreatePostDto) {
return this.postsProvider.create(dto);
}
@Get()
getAll() {
return this.postsProvider.findAll();
}
@Get('/:id')
getPostById(@Param('id') id: number) {
return this.postsProvider.findById(id);
}
}
Помимо этого в Nest.js существуют другие прикладные строительные элементы: middlewares, pipes, guards, interceptors. Ознакомиться с ними подробнее можно в официальной документации. Здесь мы рассмотрели самые основные.
Жизненный цикл
В приложениях, реализованных на Nest.js, любой модуль системы имеет свой жизненный цикл. Это позволяет назначать обработчики событий перехода из одной фазы цикла в другую.
![Стадии жизненного цикла приложения Стадии жизненного цикла приложения](https://habrastorage.org/getpro/habr/upload_files/cf9/9e6/391/cf99e63912343e0c62f38922f5fded7c.png)
Существует 4 фазы:
onModuleInit — срабатывает один раз в момент инициализации модуля;
onApplicationBootstrap — срабатывает один раз при запуске приложения;
onModuleDestroy — срабатывает один раз в момент уничтожения модуля;
onApplicationShutdown — срабатывает один раз при завершении работы приложения.
CLI
Приятным бонусом является то, что Nest.js имеет в своем наборе удобный cli-интерфейс. Для изучения поддерживаемых команд можно воспользоваться командой для вывода справки.
nest --help
Или если вам нужна справка по какой-то конкретной команде:
nest generate --help
С помощью cli можно развернуть структуру нового проекта, сгенерировать полный набор строительных элементов (контроллеры, модули, провайдеры и пр.) и модульные тесты к ним, а также управлять запуском приложения в различных режимах.
Работа с БД
Описание работы с базами данных требует отдельной статьи. Но здесь я хотел бы рассказать, что у Nest.js есть полноценная интеграция с SQL и NoSQL-базами. Предлагается на выбор несколько ORM. Наиболее популярные из них это Sequelize (@nestjs/sequelize
) и TypeORM (@nestjs/typeorm
). Для работы с MongoDB используется классический драйвер Mongoose (@nestjs/mongoose
).
Давайте рассмотрим пару примеров работы с TypeORM. Фрагмент кода, описывающий подключение к БД:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
dialect: 'postgres',
host: 'localhost',
port: 5432,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
И пример определения сущности:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
}
Тестирование
В плане тестирования у Nest.js тоже всё хорошо:
Можно автоматически генерировать кодовую структуру модульных тестов для компонентов системы и e2e-тестов для самого приложения.
Из коробки есть дефолтный test-runner, который может запускать тесты изолированно для каждого модуля.
Также имеется полноценная интеграция с популярным фреймворком для тестирования — Jest.
Используемый подход внедрения зависимостей позволяет легко мокировать компоненты системы при их запуске в тестовом окружении.
Пример простого модульного теста:
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
А это пример e2e-теста:
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
Преимущества
Предлагаю подытожить и определить преимущества, ради которых стоит обратить внимание на Nest.js:
Архитектура. В основе фреймворка лежат правильные архитектурные подходы, которые позволяют разработчику идти по уже намеченному пути и не допускать ошибок.
Проверка типов. Nest.js реализован на базе языка TypeScript. Думаю, что не стоит расписывать, какие возможности открывает этот язык. При этом не запрещено использовать обычный JavaScript.
DI. Внедрение зависимостей позволяет держать сервисы изолированными от остальной логики, а их код — более поддерживаемым и читаемым.
Тестирование. Из коробки Nest.js предоставляет полностью интегрированную и настроенную среду для написания и запуска модульных и e2e-тестов.
Заключение
Я хотел обратить внимание сообщества на Nest.js. По моему мнению, в ближайшем будущем его популярность среди разработчиков будет расти. Инструмент привносит системность в подходы к разработке сервисов на Node.js. И благодаря тому, что он первый двинулся в сторону решения архитектурных проблем, мы можем ожидать, что через пару лет он будет занимать весомую долю этой ниши.