Руководство по NestJS. Часть 1

Привет, друзья!

В этой серии из 3 статей я расскажу вам о Nest (NestJS) — фреймворке для разработки эффективных и масштабируемых серверных приложений на Node.js. Данный фреймворк использует прогрессивный (что означает текущую версию ECMAScript) JavaScript с полной поддержкой TypeScript (использование TypeScript является опциональным) и сочетает в себе элементы объектно-ориентированного, функционального и реактивного функционального программирования.

Под капотом Nest использует Express (по умолчанию), но также позволяет использовать Fastify.

В первой статье рассматриваются основы работы с Nest, во второй — некоторые продвинутые возможности, предоставляемые этим фреймворком, в третьей — приводится пример разработки простого React/Nest/TypeScript-приложения.

При рассказе о Nest я буду в основном придерживаться структуры и содержания официальной документации.

Это первая часть руководства.

Содержание:

Установка

yarn add global @nestjs/cli
# or
npm i -g @nestjs/cli

Создание проекта

# project-name - название создаваемого проекта
nest new [project-name]


Первые шаги

Выполнение команды nest new приводит к генерации следующих файлов:

src
  app.controller.spec.ts
  app.controller.ts
  app.module.ts
  app.service.ts
  main.ts


  • app.controller.ts — базовый (basic) контроллер с одним роутом (обработчиком маршрута или пути);
  • app.controller.spec.ts — юнит-тесты для контроллера;
  • app.module.ts — корневой (root) модуль приложения;
  • app.service.ts — базовый сервис с одним методом;
  • main.ts — входной (entry) файл приложения, в котором используется класс NestFactory для создания экземпляра приложения Nest.

main.ts содержит асинхронную функцию, которая инициализирует (выполняет начальную загрузку, bootstrap) приложения:

import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

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

Класс NestFactory предоставляет несколько статических методов (static methods), позволяющих создавать экземпляры приложения. Метод create возвращает объект приложения, соответствующий интерфейсу INestApplication. Этот объект предоставляет набор методов, которые будут рассмотрены в следующих разделах.

Структура проекта, создаваемого с помощью Nest CLI, предполагает размещение каждого модуля приложения в отдельной директории.

Команды для запуска приложения

Запуск приложения в производственном режиме:

yarn start
# or
npm run start

Запуск приложения в режиме для разработки:

yarn start:dev
# or
npm run start:dev


Контроллеры / Controllers

Контроллеры отвечают за обработку входящих запросов (requests) и формирование ответов (responses).


hfs_bnzhfnr4wpkmxarxx8ly6hy.png

Для создания базового контроллера используются классы и декораторы (decorators). Декораторы связывают классы с необходимыми им метаданными и позволяют Nest создавать схему маршрутизации (карту роутинга, routing map) — привязывать запросы к соответствующим контроллерам.


Маршрутизация

В приведенном ниже примере мы используем декоратор Controller для создания базового контроллера. Мы определяем опциональный префикс пути (path prefix) posts. Использование префикса пути в Controller позволяет группировать набор связанных роутов и минимизировать повторяющийся код. Например, мы можем сгруппировать набор роутов для аутентификации и авторизации с помощью префикса auth. Использование префикса пути избавляет от необходимости дублировать его в каждом роуте контроллера.

// posts.controller.ts
import { Controller, Get } from '@nestjs/common'

@Controller('posts')
export class PostController {
  @Get()
  getAllPosts(): string {
    return 'Все посты'
  }
}

Для создания контроллера с помощью Nest CLI можно выполнить команду nest g controller posts (g — generate).

Декоратор Get перед методом getAllPosts указывает Nest создать обработчик для указанной конечной точки (endpoint) для HTTP-запросов. Конечная точка соответствует методу HTTP-запроса (в данном случае GET) и пути роута. Путь роута для обработчика определяется посредством объединения опционального префикса пути контроллера и пути, указанного в декораторе метода. Поскольку мы определили префикс для каждого роута (posts) и не добавляли информацию о пути в декоратор, Nest привяжет к этому роуту запросы GET /posts. Префикс пути auth в сочетании с декоратором GET ('user') приведет к привязке к роуту запросов GET /auth/user.

В приведенном примере при выполнении GET-запросов к указанной конечной точке Nest перенаправляет запрос в метод getAllPosts. Название метода может быть любым.

Данный метод возвращает статус-код 200 и ответ в виде строки. Nest предоставляет 2 способа формирования ответов:


  • стандартный (рекомендуемый) — когда обработчик запроса возвращает объект или массив, этот объект или массив автоматически сериализуются (serialized) в JSON. Примитивные типы (строка, число, логическое значение и т.д.) возвращаются без сериализации. По умолчанию для GET-запросов статус-кодом ответа является 200, а для POST-запросов — 201: это можно изменить с помощью декоратора @HttpCode на уровне обработчика;
  • специфичный для библиотеки — мы можем использовать специфичный для библиотеки объект ответа, который может быть внедрен (встроен, injected) с помощью декоратора Res или Response в сигнатуре обработчика метода (например, getAllPosts (Res res)). В данном случае мы можем использовать методы обработки ответа, предоставляемые библиотекой, например, res.status (200).send ('Все посты').

Обратите внимание: использование в обработчике декораторов Res или Next отключает стандартный подход к обработке ответов, предоставляемый Nest. Для того, чтобы иметь возможность использовать оба подхода одновременно (например, для внедрения объекта ответа только для установки куки или заголовков) необходимо установить настройку passthrough в значение true в соответствующем декораторе, например: Res ({ passthrough: true }).


Объект запроса

Обработчикам часто требуется доступ к деталям запроса. Nest предоставляет доступ к объекту запроса используемой платформы (фреймворка). Мы можем получить доступ к объекту запроса, указав Nest внедрить его с помощью декоратора Req в обработчике:

import { Controller, Get, Req } from '@nestjs/common'
import { Request } from 'express'

@Controller('posts')
export class PostController {
  @Get()
  getAllPosts(@Req req: Request): string {
    return 'Все посты'
  }
}

Объект запроса представляет HTTP-запрос и содержит свойства для строки запроса, параметров, HTTP-заголовков и тела запроса. В большинстве случаев нам не требуется извлекать их вручную. Вместо этого, мы можем применить специальные декораторы, такие как Body или Query. Вот полный список декораторов и соответствующих им объектов:


  • @Request, @Req — req;
  • @Response, @Res — res;
  • @Next — next;
  • @Session — req.session;
  • @Param(key?: string) — req.params / req.params[key];
  • @Body(key?: string) — req.body / req.body[key];
  • @Query(key?: string) — req.query / req.query[key];
  • @Headers(name?: string) — req.headers / req.headers[name];
  • @Ip — req.ip;
  • @HostParam — req.hosts.


Ресурсы

Ранее мы определили конечную точку для получения всех постов (роут GET). Как правило, мы также хотим предоставить конечную точку для создания новых постов. Определим обработчик POST-запросов:

import { Controller, Get, Post } from '@nestjs/common'

@Controller('posts')
export class PostController {
  @Post()
  create(): string {
    return 'Новый пост'
  }

  @Get()
  getAllPosts(): string {
    return 'Все посты'
  }
}

Nest предоставляет декораторы для всех стандартных HTTP-методов: Get, Post, Put, Delete, Patch, Options и Head. Декоратор All определяет конечную точку для обработки всех методов.


Группировка путей на уровне роута / Route wildcards

Nest поддерживает роутинг на основе паттернов (регулярных выражений). Например, символ * совпадает с любой комбинацией символов:

@Get('ab*cd')
getAll(): string {
  return '*'
}

Путь ab*cd будет совпадать с abcd, _abcd, abecd и т.д. В пути роута также могут использоваться символы ? , + и (). Символы - и . интерпретируются буквально.


Статус-код ответа

Декоратор @HttpCode позволяет определять статус-код ответа:

// немного забегая вперед
import { Delete, Param, HttpCode } from 'nestjs/common'
import { GetUser } from 'src/auth/decorator'

@Delete()
@HttpCode(204)
remove(
  @Param('id', ParseIntPipe) postId: number,
  @GetUser('id') userId: number
) {
  return this.postService.remove({ postId, userId })
}

Вместо явного определения статус-кода можно использовать значение из перечисления (enum) HttpStatus:

import { Delete, HttpCode, HttpStatus } from 'nestjs/common'

@Delete()
@HttpCode(HttpStatus.NO_CONTENT)


Заголовки ответа

Декоратор Header позволяет определять заголовки ответа:

@Post()
@Header('Cache-Control', 'no-cache, no-store, must-revalidate')
create(): string {
  return 'Новый пост'
}


Перенаправление запроса

Для перенаправления запроса предназначен декоратор Redirect. Он принимает 2 опциональных аргумента: url и statusCode. Последний по умолчанию имеет значение 302.

@Get()
@Redirect('https://redirected.com', 301)

В случае, когда url или statusCode определяются динамически, для выполнения перенаправления из обработчика можно вернуть такой ответ:

{
  url: string,
  statusCode: number
}

Возвращаемые значения перезаписывают аргументы, переданные в Redirect:

@Get()
@Redirect('https://redirected.com')
get(@Query('version') version: string) {
  if (version) {
    return {
      url: `https://redirected.com/v${version}`
    }
  }
}


Параметры запроса

Роуты со статическими путями не будут работать в случаях, когда путь включает динамические данные, являющиеся частью запроса (например, GET posts/1 для получения поста с идентификатором 1). Для решения этой задачи мы можем добавить токены (tokens) параметров в путь роута для перехвата динамического значения на указанной позиции в URL запроса. Доступ к параметрам роута можно получить с помощью декоратора Param, который должен быть добавлен в сигнатуру метода:

@Get(':id')
getPostById(@Param() params: Record): string {
  console.log(params.id)
  return `Пост с идентификатором ${params.id}`
}

В Param мы можем явно указать интересующий нас параметр:

@Get(':id')
getPostById(@Param('id') id: string): string {
  return `Пост с идентификатором ${id}`
}


Тело запроса

Для получения доступа к телу запроса предназначен декоратор Body.

Для определения контракта (contract), которому должен соответствовать объект тела запроса в Nest используется схема объекта передачи данных (Data Transfer Object, DTO), реализуемая в виде класса. Почему не в виде интерфейса TypeScript? Потому что классы являются частью ECMAScript и остаются в скомпилированном JavaScript (в отличие от интерфейсов, которые удаляются при преобразовании TypeScript в JavaScript). В ряде случаев (например, при использовании Pipes, о которых мы поговорим в одном из следующих разделов), Nest требуется доступ к метаданным переменной во время выполнения кода.

Определим класс CreatePostDto:

// create-post.dto
export class CreatePostDto {
  title: string,
  content: string,
  authorId: number
}

И добавим его в PostController:

// post.controller.ts
@Post()
async create(@Body() createPostDto: CreatePostDto) {
  return this.postService.create(createPostDto)
}


Расширенный пример контроллера

import { Controller, Get, Query, Post, Body, Put, Param, Delete, HttpCode, HttpStatus } from '@nestjs/common'
import { CreatePostDto, UpdatePostDto, ListAllEntities } from './dto'
import { PostService } from './post.service'

@Controller('posts')
export class PostController {
  constructor(private postService: BookmarkService) {}

  @Post()
  create(@Body() createPostDto: CreatePostDto) {
    return 'Новый пост'
  }

  @Get()
  getAllPosts(@Query() query: ListAllEntities) {
    return `Посты в количестве ${query.limit} штук`
  }

  @Get(':id')
  getPostById(@Param('id') id: string) {
    return `Пост с идентификатором ${id}`
  }

  @Put(':id')
  update(
    @Param('id') id: string,
    @Body() updatePostDto: UpdatePostDto
  ) {
    return `Обновленный пост с идентификатором ${id}`
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    return this.postService.remove(id)
  }
}


Подключение контроллера к модулю

Для того, чтобы сообщить Nest о существовании контроллера PostController, его необходимо передать в массив контроллеров модуля:

// app.module.ts
import { Module } from '@nestjs/common'
import { PostController } from 'post/post.controller'

@Module({
  controllers: [PostController]
})
export class AppModule {}


Провайдеры / Providers

Провайдеры — фундаментальная концепция Nest. В роли провайдеров могут выступать многие базовые классы Nest — сервисы (services), репозитории (repositories), фабрики (factories), помощники (helpers) и др. Суть провайдера состоит в том, что он может быть внедрен (injected) в качестве зависимости. Это означает, что объекты могут выстраивать различные отношения с другими объектами. Функции по созданию экземпляров объектов во многом могут быть делегированы системе выполнения (runtime system) Nest.


fyw2knbrfnkeuurcxr21kpkkrd4.png

Провайдеры — это обычные JS-классы, которые определяются как providers в модуле.


Сервисы / Services

Создадим простой сервис PostService. Данный сервис будет отвечать за хранение и извлечение данных. Он спроектирован для использования в PostController, что делает его отличным кандидатом на статус провайдера.

// post.service.ts
import { Injectable } from '@nestjs/common'
import { PostDto, CreatePostDto } from './dto'

@Injectable()
export class PostService {
  private readonly posts: PostDto[] = []

  create(post: CreatePostDto) {
    this.posts.push(post)
  }

  getAllPosts(): PostDto[] {
    return this.posts
  }
}

Для создания сервиса с помощью Nest CLI можно использовать команду nest g service post.

Наш PostService — обычный класс с одним свойством и двумя методами. Новым является декоратор @Injectable. Этот декоратор добавляет метаданные, которые передают управление PostService контейнеру инверсии управления (Inversion of Control, IoC) Nest. В примере также используется интерфейс PostDto, который может выглядеть так:

// dto/post.dto.ts
export class PostDto {
  id: number
  title: string
  content: string
  authorId: number
  createdAt: Date
}

Вот как можно использовать созданный нами сервис в контроллере:

// post.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common'
import { CreatePostDto } from './dto/create-post.dto'
import { PostService } from './post.service'
import { PostDto, CreatePostDto } from './dto'

@Controller('post')
export class PostController {
  constructor(private postService: PostService) {}

  @Post()
  async create(@Body() createPostDto: CreatePostDto) {
    this.postService.create(createPostDto)
  }

  @Get()
  async getAllPosts(): Promise {
    return this.postService.getAllPosts()
  }
}

PostService внедряется в контроллер через конструктор класса. Обратите внимание на использование ключевого слова private. Такое сокращение позволяет одновременно определить и инициализировать поле postService в одном месте.


Внедрение зависимостей

В основе Nest лежит мощный паттерн, известный под названием «внедрение зависимостей» (Dependency Injection).

В Nest благодаря возможностям, предоставляемым TypeScript, управлять зависимостями очень легко, поскольку они разрешаются (resolved) по типу. В приведенном примере Nest разрешает postService посредством создания и возврата экземпляра PostService (обычно, возвращается существующий экземпляр класса — паттерн «Одиночка» / Singleton). Данная зависимость разрешается и передается конструктору контроллера (или присваивается указанному свойству):

constructor(private postService: PostService) {}


Регистрация провайдеров

У нас имеется провайдер (PostService) и его потребитель (PostController). Теперь нам необходимо зарегистрировать сервис для того, чтобы Nest мог осуществить его внедрение. Это делается путем добавления сервиса в массив, передаваемый полю providers декоратора Module:

// app.module.ts
import { Module } from '@nestjs/common'
import { PostController } from './post/post.controller'
import { PostService } from './post/post.service'

@Module({
  controllers: [PostController],
  providers: [PostService]
})
export class AppModule {}

После этого Nest будет иметь возможность разрешать зависимости PostController.

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

src
  post
    dto
      post.dto.ts
      create-post.dto.ts
    post.controller.ts
    post.service.ts
  app.module.ts
  main.ts


Модули / Modules

Модуль — это класс, аннотированный с помощью декоратора Module. Этот декоратор предоставляет Nest метаданные, необходимые для организации структуры приложения.


wcvzl8lfdbneof2fsdc4omzmap8.png

В каждом приложении имеется по крайней мере один модуль — корневой (root). Корневой модуль — это начальная точка для построения графа приложения (application graph) — внутренней структуры данных, используемой Nest для разрешения модулей и построения отношений и зависимостей. В большинстве приложений будет использоваться несколько модулей, инкапсулирующих близкий набор возможностей.

Декоратор Module принимает объект со следующими свойствами:


  • providers — провайдеры, которые инстанцируются Nest и могут использоваться любыми частями, по крайней мере, данного модуля;
  • controllers — набор контроллеров, определенных в данном модуле для инстанцирования;
  • imports — список импортируемых модулей, которые экспортируют провайдеры, необходимые данному модулю;
  • exports — часть провайдеров, принадлежащих данному модулю, которые должны быть доступны другим модулям. Мы можем использовать сам провайдер или только его токен (значение provide).

Модуль инкапсулирует провайдеры по умолчанию. Это означает, что невозможно внедрить провайдеры, которые не являются частью данного модуля и не экспортируются из импортируемых им модулей. Поэтому экспортируемые провайдеры можно считать часть публичного интерфейса (API) модуля.


Модули частей приложения

PostController и PostService принадлежат одному и тому же домену (domain) приложения. Поскольку они тесно связаны между собой, имеет смысл вынести их в отдельный модуль. Частичный модуль просто организует код, отвечающий за определенную часть (возможность) приложения. Это помогает управлять сложностью приложения и разрабатывать, придерживаясь принципов SOLID, что становится особенно актуальным с ростом приложения или команды разработчиков.

Создадим PostModule:

// post.module.ts
import { Module } from '@nestjs/common'
import { PostController } from './post.controller'
import { PostService } from './post.service'

@Module({
  controllers: [PostController],
  providers: [PostService]
})
export class PostModule {}

Для создания модуля с помощью Nest CLI можно выполнить команду nest g module post.

Импортируем этот модуль в корневой (AppModule, определенный в app.module.ts):

import { Module } from '@nestjs/common'
import { PostModule } from './post/post.module'

@Module({
  imports: [PostModule]
})
export class AppModule {}

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

src
  post
    dto
      post.dto.ts
      create-post.dto.ts
    post.controller.ts
    post.module.ts
    post.service.ts
  app.module.ts
  main.ts


Распределенные модули

По умолчанию модули в Nest являются «одиночками». Поэтому мы можем распределять один и тот же экземпляр любого провайдера между несколькими модулями.


gnmnjdphzkyivhtmzomf6qsy38k.png

Каждый модуль по умолчанию является распределенным (shared). Предположим, что мы хотим поделиться экземпляром PostService с другими модулями. Для этого нужно экспортировать PostService из модуля:

import { Module } from '@nestjs/common'
import { PostController } from './post.controller'
import { PostService } from './post.service'

@Module({
  controller: [PostController],
  providers: [PostService],
  exports: [PostService]
})
export class PostModule {}

Теперь любой модуль, импортирующий PostModule, будет иметь доступ к PostService и будет делиться этим экземпляром с модулями, импортирующими его самого.


Повторный экспорт модулей

Модуль может экспортировать не только внутренние провайдеры, но и импортируемые модули:

@Module({
  imports: [CommonModule],
  exports: [CommonModule]
})
export class CoreModule {}


Внедрение зависимостей

Модуль также может внедрять провайдеры:

import { Module } from '@nestjs/common'
import { PostController } from './post.controller'
import { PostService } from './post.service'

@Module({
  controllers: [PostController],
  providers: [PostService]
})
export class PostModule {
  constructor(private postService: PostService) {}
}

Тем не менее, сами модули не могут внедряться как провайдеры по причине циклической зависимости (circular dependency).


Глобальные модули

Для создания глобального модуля (помощники, ORM и т.д.) предназначен декоратор Global:

import { Module, Global } from '@nestjs/common'
import { PostController } from './post.controller'
import { PostService } from './post.service'

@Global()
@Module({
  controllers: [PostController],
  providers: [PostService],
  exports: [PostService]
})
export class PostModule {}

Декоратор Global делает модуль глобальным. Такие модули регистрируются только один раз, обычно, в корневом модуле. В приведенном примере провайдер PostService будет доступен любому модулю без необходимости импорта PostModule.


Посредники / Middleware

Посредник — это функция, которая вызывается перед обработчиком маршрута. Посредники имеют доступ к объектам запроса и ответа, а также к посреднику next в цикле запрос-ответ.


bqbkegi8pc_k8nne0ahtgakvpd0.png

По умолчанию посредники Nest аналогичны посредникам Express.

Посредники предоставляют следующие возможности:


  • выполнение любого кода;
  • модификация объектов запроса и ответа;
  • завершение цикла запрос-ответ;
  • вызов следующего посредника в стеке;
  • если текущий посредник не завершает цикл запрос-ответ, он должен вызвать next () для передачи управления следующему посреднику. В противном случае, запрос зависнет (hanging).

Кастомный посредник может быть реализован как в виде функции, так и в виде класса с помощью декоратора @Injectable. Класс должен реализовывать интерфейс NestMiddleware, а к функции особых требований не предъявляется. Начнем с реализации посредника в виде класса:

import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Запрос...')
    next()
  }
}


Внедрение зависимостей

Посредники Nest поддерживают внедрение зависимостей. Как и провайдеры или контроллеры, они могут внедрять зависимости, доступные в родительском модуле. Это делается через constructor.


Регистрация посредников

В декораторе Module нет места для посредников. Поэтому мы применяем их в с помощью метода configure класса модуля. Модули, включающие посредников, должны реализовывать интерфейс NestModule. Применим LoggerMiddleware на уровне AppModule:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
import { LoggerMiddleware } from './middlewares/logger.middleware'
import { PostModule } from './post/post.module'

@Module({
  imports: [PostModule]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('post')
  }
}

В приведенном примере мы применяем LoggerMiddleware к обработчикам маршрутов /post, определенным в PostController. Ограничить обработчики, к которым применяется посредник, можно следующим образом (обратите внимание на использование перечисления RequestMethod):

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common'
import { LoggerMiddleware } from './middlewares/logger.middleware'
import { PostModule } from './post/post.module'

@Module({
  imports: [PostModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'post', method: RequestMethod.GET })
  }
}

Обратите внимание: метод configure может быть асинхронным (async/await).


Перехват роутов / Route wildcards

Метод forRoutes поддерживает перехват роутов с помощью паттернов:

forRoutes({ path: 'ab*cd', method: RequestMethod.ALL })


Потребитель посредника / Middleware consumer

MiddlewareConsumer — это вспомогательный класс. Он предоставляет несколько встроенных методов для управления посредником. Данные методы могут вызываться по цепочке. Метод forRoutes принимает строку, несколько строк или объект RouteInfo, класс контроллера или несколько таких классов. В большинстве случаев мы передаем этому методу контроллеры, разделенные запятыми. Пример передачи одного контроллера:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
import { LoggerMiddleware } from './middlewares/logger.middleware'
import { PostModule } from './post/post.module'
import { PostController } from './post/post.controller'

@Module({
  imports: [PostModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(PostController)
  }
}

Обратите внимание: метод apply может принимать несколько посредников.


Исключение роутов

Иногда мы не хотим, чтобы посредник применялся к определенным роутам. Исключить такие роуты можно с помощью метода exclude, который принимает строку, несколько строк или объект RouteInfo:

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'post', method: RequestMethod.GET },
    { path: 'post', method: RequestMethod.POST },
    'post/(.*)',
  )
  .forRoutes(CatsController)

Обратите внимание: метод exclude поддерживает перехват роутов с помощью паттернов.


Посредники в виде функций

Реализованный нами посредник LoggerMiddleware является очень простым. У него нет членов, дополнительных методов и зависимостей. Поэтому мы вполне может реализовать его в виде функции:

import { Request, Response, NextFunction } from 'express'

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log('Запрос...')
  next()
}

Применяем его в AppModule:

consumer
  .apply(logger)
  .forRoutes(PostController)


Несколько посредников

Для применения нескольких посредников достаточно передать их методу apply через запятую:

consumer.apply(cors(), helmet(), logger).forRoutes(PostController)


Глобальные посредники

Для применения посредника ко всем роутам приложения можно использовать метод use, предоставляемый экземпляром INestApplication:

const app = await NestFactory.create(AppModule)
app.use(logger)
await app.listen(3000)


Фильтры исключений / Exception filters

Nest имеет встроенный слой для обработки исключений (exceptions layer), которые по какой-то причине не были обработаны приложением (unhandled exceptions — необработанные исключения).


mw6ztzk3lzscdpzzs4g1t-avijg.png

По умолчанию используется глобальный фильтр исключений (global exception filter), который обрабатывает исключения типа HttpException (и его подклассов). В случае, когда исключение не удается распознать (когда оно не является ни HttpException, ни классом, наследующим от него), генерируется такой ответ в формате JSON:

{
  "statusCode": 500,
  "message": "Internal server error"
}


Стандартные исключения

Nest предоставляет встроенный класс HttpException. В PostController у нас имеется метод getAllPosts. Предположим, что по какой-то причине обработчик этого роута выбрасывает исключение:

import { HttpException, HttpStatus } from '@nestjs/common'

@Get()
async getAllPosts() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
}

В ответ на запрос к данной конечной точке возвращается такой ответ:

{
  "statusCode": 403,
  "message": "Forbidden"
}

Конструктор HttpException принимает 2 обязательных параметра, определяющих ответ:


  • параметр response определяет тело ответа. Он может быть строкой или объектом;
  • параметр status определяет статус-код.

По умолчанию тело ответа содержит 2 свойства:


  • statusCode: статус-код, указанный в аргументе status;
  • message: краткое описание ошибки на основе status.

Для перезаписи message достаточно передать строку в качестве response. Для перезаписи всего тела ответа в качестве response передается такой объект:

@Get()
async getAllPosts() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'Доступ запрещен'
  }, HttpStatus.FORBIDDEN)
}

В этом случае ответ на запрос будет выглядеть так:

{
  "status": 403,
  "error": "Доступ запрещен"
}


Встроенные исключения

Nest предоставляет набор исключений, наследующих от HttpException. Они экспортируются из @nestjs/common и представляют наиболее распространенные исключения:


  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException


Фильтры исключений

Фильтры исключений позволяют получить полный контроль над процессом обработки исключения и формированием содержимого ответа.

Определим фильтр исключений, отвечающий за перехват исключений, которые являются экземплярами класса HttpException, и реализующий кастомную логику формирования ответа. Для этого нам потребуется доступ к объектам Request и Response. Объект Request будет использоваться для извлечения URL и его включения в ответ. Объект Response будет использоваться для отправки ответа с помощью метода json:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const res = ctx.getResponse()
    const req = ctx.getRequest()
    const status = exception.getStatus()

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: req.url
      })
  }
}

Обратите внимание: все фильтры исключений должны реализовывать общий интерфейс ExceptionFilter. Сигнатура определяется с помощью catch(exception: T, host: ArgumentsHost), где T — это тип исключения.

Декоратор Catch привязывает необходимые метаданные к фильтру исключений. Это сообщает Nest, что данный фильтр обрабатывает только исключения типа HttpException. Декоратор Catch может принимать несколько аргументов, разделенных через запятую, для обработки нескольких типов исключений.


Аргументы хоста / Arguments host

В приведенном примере мы используем объект ArgumentsHost для получения доступа к объектам Request и Response. Вспомогательные функции, предоставляемые ArgumentsHost, позволяют получать доступ к любому контексту выполнения, будь то HTTP-сервер, микросервисы или веб-сокеты.


Применение фильтров исключений

Привяжем HttpExceptionFilter к методу create в PostController:

import { Post, UseFilters, Body, ForbiddenException } from '@nestjs/common'

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body createPostDto: CreatePostDto) {
  throw new ForbiddenException()
}

Декоратор @UseFilters может принимать несколько фильтров через запятую.

Фильтры исключений могут применяться не только на уровне методов, но также на уровне контроллеров и даже глобально. Пример использования фильтра на уровне контроллера:

@UseFilters(HttpExceptionFilter)
export class PostController {}

Пример использования фильтра на уровне всего приложения (глобально):

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


Перехват всех исключений

Для того, чтобы перехватывать все необработанные исключения (независимо от типа исключения), достаточно применить декоратор Catch без аргументов:

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common'
import { HttpAdapterHost } from '@nestjs/core`'

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // В некоторых случаях `httpAdapter` может быть недоступен в конструкторе,
    // поэтому нам следует получать (разрешать) его здесь
    const { httpAdapter } = this.httpAdapterHost

    const ctx = host.switchToHttp()

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    }

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus)
  }
}

HttpAdapterHost позволяет коду быть платформонезависимым (platform-agnostic), поскольку мы не используем зависящие от платформы объекты Request и Response напрямую.


Конвейеры / Pipes

Конвейер — это класс, аннотированный с помощью декоратора @Injectable и реализующий интерфейс PipeTransform.


vsewqpupaklnyrzt0ysmpufs07k.png

Конвейеры используются для:


  • трансформации: преобразование входных данных в ожидаемый формат, например, преобразование строки в число;
  • валидации: проверка корректности входных данных.

Nest запускает конвейер перед вызовом обработчика маршрута, и конвейер получает аргументы, переданные последнему.

Nest предоставляет несколько встроенных конвейеров. Разумеется, можно создавать собственные конвейеры.


Встроенные конвейеры

Nest предоставляет 8 встроенных конвейеров:


  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe

Встроенные конвейеры экспортируются из пакета @nestjs/common.

Далее мы рассмотрим пример использования ParseIntPipe для преобразования аргумента, переданного обработчику, в целое число (при невозможности такого преобразования выбрасывается исключение).


Регистрация конвейеров

Для применения конвейера нам необходимо привязать его экземпляр к соответствующему контексту. На уровне метода это можно сделать следующим образом:

@Get(':id')
async getPostById(@Param('id', ParseIntPipe) id: number) {
  return this.postService.getPostById(id)
}

После этого если мы отправим GET-запрос к конечной точке http://localhost:3000/abc, Nest выбросит такое исключение:

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

В этом случае код метода getPostById не выполняется.

Для кастомизации поведения встроенного конвейера вместо класса передается его экземпляр:

@Get(':id')
async getPostById(
  @Param('id', new ParseIntPipe({
    errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE
  }))
  id: number
) {
  return this.postService.getPostById(id)
}


Кастомные конвейеры

Реализуем простой конвейер ValidationPipe, принимающий значение и просто возвращающий его:

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common'

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value
  }
}

Обратите внимание: PipeTransform — это общий интерфейс, который должен быть реализован любым конвейером. T — это тип входного значения (value), а R — тип значения, возвращаемого методом transform.

transform () принимает 2 параметра:


  • value — аргумент, переданный обработчику;
  • metadata — объект со следующими свойствами:
    • type — тип аргумента: 'body' | 'query' | 'param' | 'custom';
    • metatype — тип данных аргумента, например, String;
    • data — строка, переданная декоратору, например, @Body('string').


Валидация входных данных на основе схемы

Сделаем наш конвейер для валидации более полезным. Предположим, что мы хотим валидировать объект тела запроса, передаваемого методу create. Как нам это сделать?

Существует несколько способов валидации объекта. Одним из наиболее распр

© Habrahabr.ru