Как стать автором
Обновить
1524.37
Timeweb Cloud
То самое облако

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

Время на прочтение34 мин
Количество просмотров98K



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


В этой серии из 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).





Для создания базового контроллера используются классы и декораторы (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, @Reqreq;
  • @Response, @Resres;
  • @Nextnext;
  • @Sessionreq.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];
  • @Ipreq.ip;
  • @HostParamreq.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, string>): 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.





Провайдеры — это обычные 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<PostDto[]> {
    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 метаданные, необходимые для организации структуры приложения.





В каждом приложении имеется по крайней мере один модуль — корневой (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 являются "одиночками". Поэтому мы можем распределять один и тот же экземпляр любого провайдера между несколькими модулями.





Каждый модуль по умолчанию является распределенным (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 в цикле запрос-ответ.





По умолчанию посредники 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 — необработанные исключения).





По умолчанию используется глобальный фильтр исключений (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<Response>()
    const req = ctx.getRequest<Request>()
    const status = exception.getStatus()

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

Обратите внимание: все фильтры исключений должны реализовывать общий интерфейс ExceptionFilter<T>. Сигнатура определяется с помощью 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.





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


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

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, R> — это общий интерфейс, который должен быть реализован любым конвейером. T — это тип входного значения (value), а R — тип значения, возвращаемого методом transform.


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


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

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


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


Существует несколько способов валидации объекта. Одним из наиболее распространенных является валидация на основе схемы (schema-based validation). Одной из наиболее популярных библиотек для такой валидации является Joi.


Устанавливаем необходимые зависимости:


yarn add joi
yarn add -D @types/joi

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

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value)

    if (error) {
      throw new BadRequestException('Валидация провалилась')
    }

    return value
  }
}

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


  1. Создать экземпляр JoiValidationPipe.
  2. Передать схему в конструктор класса конвейера.
  3. Привязать конвейер к методу.

@Post()
@UsePipes(new JoiValidationPipe(createPostSchema))
async create(@Body() createPostDto: CreatePostDto) {
  this.postService.create(createPostDto)
}

Существует другой, более простой способ: библиотека class-validator позволяет выполнять валидацию с помощью декораторов.


Устанавливаем необходимые зависимости:


yarn add class-validator class-transformer

Добавляем несколько декораторов-валидаторов в класс CreatePostDto


// create-post.dto
import { IsString, IsInt } from 'class-validator'

export class CreatePostDto {
  @IsString()
  title: string,

  @IsString()
  content: string,

  @IsInt()
  authorId: number
}

Обновляем код ValidationPipe:


import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || this.toValidate(metatype)) {
      return value
    }

    const obj = plainToClass(metatype, value)
    const errors = await validate(object)

    if (errors.length > 0) {
      throw new BadRequestException('Валидация провалилась')
    }

    return value
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object]
    return !types.includes(metatype)
  }
}

Обратите внимание: библиотека class-transformer разработана тем же человеком, что и class-validator, поэтому они очень хорошо работают вместе.


Вспомогательная функция toValidate предназначена для пропуска валидации, когда обрабатываемый аргумент имеет нативный тип JavaScript (к такому аргументу нельзя добавить декораторы для валидации, поэтому выполнять ее бессмысленно).


Функция plainToClass предназначена для преобразования обычного объекта JavaScript в типизированный объект для применения валидации. Причина необходимости такого преобразования состоит в том, что входной объект тела запроса не содержит информации о типах, а class-validator нуждается в них для применения декораторов для валидации, определенных нами в CreatePostDto.


Привяжем ValidationPipe к декоратору Body:


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

Конвейеры могут применяться на уровне параметра, метода, контроллера или глобально.


Пример глобального использования конвейера:


// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(ValidationPipe)
  await app.listen(3000)
}

Встроенный конвейер для валидации


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


Кастомный конвейер для трансформации


Как отмечалось ранее, конвейеры могут использоваться не только для валидации, но и для трансформации. Рассмотрим пример кастомной реализации ParseIntPipe:


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

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10)

    if (isNaN(val)) {
      throw new BadRequestException('Переданный аргумент не может быть преобразован в число')
    }

    return val
  }
}

Применяем этот конвейер:


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

Конвейер для передача параметров по умолчанию


Конвейер DefaultValuePipe позволяет определять дефолтные параметры:


@Get()
async getAllPosts(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('published', new DefaultValuePipe(true), ParseBoolPipe) published: boolean
) {
  return this.postService.getAllPosts({ page, published })
}

Защитники/предохранители / Guards


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





Защитники предназначены для авторизации — определения того, передавать ли запрос обработчику маршрута для дальнейшей обработки, в зависимости от определенных условий (разрешения, роли пользователя и т.д.). Защитники, в отличие от посредников, имеют доступ к экземпляру ExecutionContext и по этой причине точно знают, какой код будет выполняться следующим.


Обратите внимание: защитники выполняются после посредников, но перед перехватчиками и конвейерами.


Защитник для авторизации


Реализуем простой защитник для авторизации:


import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const req = context.switchToHttp().getRequest()

    return validateRequest(req)
  }
}

Логика внутри validateRequest может быть настолько простой или сложной, насколько необходимо.


Функция canActivate должна возвращать логическое значение — индикатор валидности текущего запроса:


  • если она возвращает true, запрос будет передан обработчику;
  • если она возвращает false, запрос будет отклонен с ошибкой.

Контекст выполнения / Execution context


canActivate() принимает один параметр — экземпляр ExecutionContext. ExecutionContext наследует от ArgumentsHost. ExecutionContext расширяет ArgumentsHost несколькими вспомогательными методами, предоставляющими дополнительную информацию о текущем процессе выполнения (обработке запроса).


Аутентификация на основе роли пользователя / Role-based authentication


Реализуем более функциональный защитник, предоставляющий доступ только пользователям с определенной ролью. Начнем с определения базового защитника, разрешающего все запросы:


import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true
  }
}

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


Защитники, также как и исключающие фильтры или конвейеры могут привязываться к методу, контроллеру или быть глобальными. Для привязки защитника используется декоратор @UseGuards из пакета @nestjs/common, в качестве параметра принимающий одного или нескольких защитников, разделенных запятыми:


@Controller('posts')
@UseGuards(RolesGuard)
export class PostController {}

Для установки глобального защитника используется метод useGlobalGuard экземпляра приложения Nest:


const app = await NestFactory.create(AppModule)
app.useGlobalGuard(RolesGuard)

Определение ролей для обработчика


Наш RolesGuard работает, но пока он бесполезен. postController может иметь разные схемы разрешений: одни роуты могут быть доступны всем, другие — только администраторам.


Вот где нам пригодятся кастомные метаданные. Для их добавления предназначен декоратор @SetMetadata из пакета @nestjs/common:


@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createPostDto: CreatePostDto) {
  this.postService.create(createPostDto)
}

Мы добавляем метаданные roles к методу create (roles — о ключ, а ['admin'] — значение). На практике @SetMetadata редко используется напрямую. Давайте вынесем его в кастомный декоратор:


import { SetMetadata } from '@nestjs/common'

export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

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


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

Полный пример


Для доступа к кастомным метаданным в защитнике используется вспомогательный класс Reflector:


import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', ctx.getHandler())

    if (!roles) {
      return true
    }

    const req = ctx.switchToHttp().getRequest()

    return matchRoles(roles, req.user.role)
  }
}

В приведенном примере мы исходим из предположения, что в результате аутентификации объект с данными пользователя был записан в объект запроса.


Если пользователь, который не является администратором, попытается создать пост, он получит такую ошибку:


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

По умолчанию Nest выбрасывает исключение ForbiddenException. Однако в защитнике можно выбрасывать любые другие исключения:


throw new UnauthorizedException()

Исключения, выбрасываемые защитниками, обрабатываются соответствующим слоем (exception layer).


Перехватчики / Interceptors


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





Возможности перехватчиков:


  • выполнение логики перед/после вызова метода;
  • преобразования результата вызова функции;
  • преобразования исключения, выброшенного методом;
  • расширение поведения функции;
  • полная перезапись функции в зависимости от определенных условий (например, в целях кеширования).

Основы


Каждый перехватчик реализует метод intercept, принимающий 2 параметра. Первым параметром является экземпляр контекста выполнения (ExecutionContext), о котором рассказывалось в разделе, посвященном защитникам. Второй параметр — обработчик вызова (CallHandler).


CallHandler


Интерфейс CallHandler реализует метод handle, который используется для вызова метода обработчика маршрута для передачи ему управления. Если не вызвать handle в intercept, метод обработчика не будет выполнен.


Перехватчики позволяют выполнять кастомную логику как до, так и после вызова метода обработчика. С "до" все ясно, но что позволяет перехватчику выполнять код "после"? Дело в том, что handle возвращает Observable. Это позволяет использовать мощные операторы RxJS для дальнейшей манипуляции ответом. В терминологии аспектно-ориентированного программирования это называется точечным разрезом (pointcut) — мы вставляем в эту точку дополнительную логику.


Перехват аспекта / Aspect interception


Первым случаем использования перехватчиков является фиксирование взаимодействия пользователя со страницей (например, сохранение действий пользователя, асинхронный вызов событий или вычисление отметки времени (timestamp)). Реализуем простой перехватчик LoggingInterceptor:


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: , next: ): Observable<any> {
    console.log('Перед...')

    const now = Date.now()

    return next
      .handle()
      .pipe(
        tap(() => console.log(`После... ${Date.now() - now} мс`))
      )
  }
}

NestInterceptor<T, R> — общий интерфейс, в котором T означает тип Observable<T> (поддерживающего поток ответа — response stream), а R — тип значения, обернутого Observable<R>.


Обратите внимание: перехватчики, как и контроллеры, провайдеры, защитники и др. могут внедрять зависимости через constructor.


Поскольку handle() возвращает Observable, в нашем распоряжении имеется большое количество операторов для работы с потоком.


Регистрация перехватчиков


Для установки перехватчика используется декоратор @UseInterceptor из пакета @nestjs/common. Подобно конвейерам или защитникам, перехватчики могут устанавливаться на уровне контроллеров, методов или глобально.


@UseInterceptors(LoggingInterceptor)
export class PostController {}

Для установки глобального перехватчика используется метод useGlobalInterceptor экземпляра приложения Nest:


const app = await NestFactory.create(AppModule)
app.useGlobalInterceptor(LoggingInterceptor)

Обработка ответов


Как мы знаем, метод handle возвращает Observable. Поток содержит значение из обработчика маршрута, которое можно мутировать с помощью оператора map из библиотеки RxJS.


Создадим TransformInterceptor, в котором map используется для присвоения объекта ответа свойству data нового объекта, возвращаемого клиенту:


import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'

export interface Response<T> {
  data: T
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(map((data) => ({ data })))
  }
}

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


return next
  .handle()
  .pipe(map((value) => value === null ? '' : value ))

Обработка исключений


Еще одним интересным случаем использования перехватчиков является обработка исключений с помощью оператора catchError из библиотеки RxJS:


import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  BadGatewayException,
  CallHandler
} from '@nestjs/common'
import { Observable, throwError } from 'rxjs'
import { catchError } from 'rxjs/operators'

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        catchError((err) => throwError(() => new BadGatewayException()))
      )
  }
}

Перезапись потока ответа


Существует несколько причин, по которым мы можем захотеть отключить вызов обработчика маршрута. Одной из таких причин является, например, доставка контента из кеша вместо обращения к базе данных. Рассмотрим пример такого перехватчика:


import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
import { Observable, of } from 'rxjs'

@Injectable()
export class CacheInterceptor implements ... {
  intercept(ctx: ..., next: ...): Observable<any> {
    const isCached = true

    if (isCached) {
      return of([])
    }

    return next.handle()
  }
}

У нас имеется жестко заданная переменная isCached и жестко заданный ответ []. В данном случае мы возвращаем новый поток, генерируемый оператором of, поэтому обработчик маршрута не вызывается.


Другие операторы RxJS


Тот факт, что мы можем манипулировать потоком ответа с помощью операторов RxJS, предоставляет в наше распоряжение много интересных возможностей. Допустим, мы хотим обрабатывать таймауты — прекращать выполнение запроса по истечении определенного периода времени.


import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common'
import { Observable, throwError, TimeoutError } from 'rxjs'
import { catchError, timeout } from 'rxjs/operators'

@Injectable()
export class TimeoutInterceptor implements ... {
  intercept(ctx: ..., next: ...): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError((err) => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException())
        }
        return throwError(() => err)
      })
    )
  }
}

В приведенном примере запрос, который обрабатывается дольше 5 секунд, отменяется.


Кастомные декораторы маршрутов / Custom route decorators


Nest позволяет легко создавать кастомные декораторы. Когда это может пригодиться?


В Node.js распространенной практикой является добавление новых свойств в объект запроса. Затем эти свойства извлекаются в обработчиках запросов.


const { user } = req

Для того, чтобы сделать код более читаемым и прозрачным, можно создать такой декоратор User:


import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const User = createParamDecorator(
  (data: unknown, ctx: ...) => {
    const req = ctx.switchToHttp().getRequest()

    return req.user ? req.user : null
  }
)

Затем мы просто применяем этот декоратор по необходимости:


@Get()
async getPostById(@User() user: User) {
  console.log(user)
}

Передача данных


Когда поведение декоратора зависит от некоторых условий, мы можем использовать параметр data для передачи аргументов фабричной функции декоратора. Одним из таких случаев является извлечение свойств из объекта запроса по ключу. Предположим, что наш слой для аутентификации валидирует запрос и добавляет пользователя в объект запроса. Сущность пользователя может выглядеть так:


{
  "id": 1,
  "firstName": "Алан",
  "lastName": "Тьюринг",
  "email": "alan@mail.com",
  "roles": ["admin"]
}

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


import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const User = createParamDecorator(
  (data: string, ctx: ...) => {
    const req = ctx.switchToHttp().getRequest()
    const { user } = req

    if (!user) return null

    return data ? user[data] : user
  }
)

Пример использования этого декоратора:


@Get()
async getPostById(@User('firstName') firstName: string) {
  console.log(`Привет, ${firstName}!`)
}

Мы можем использовать этот декоратор с разными ключами для доступа к разным свойствам.


Работа с конвейерами


Конвейеры могут применяться к кастомным декораторам напрямую:


@Get()
async getPostById(
  @User(new ValidationPipe({ validateCustomDecorators: true }))
  user: User
) {
  console.log(user)
}

Обратите внимание: настройка validateCustomDecorators должна быть установлена в значение true.


Композиция декораторов


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


import { applyDecorators } from '@nestjs/common'

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' })
  )
}

Пример использования этого декоратора:


@Get('users')
@Auth('admin')
getAllUsers()

На этом первая часть руководства завершена.


Благодарю за внимание и happy coding!




Теги:
Хабы:
Всего голосов 12: ↑10 и ↓2+10
Комментарии4

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud