Строим REST API с использованием Nest.js и Swagger

от автора

Введение

Друзья, всем привет! 

Меня зовут Алексей и вот уже некоторое время я занимаюсь frontend-разработкой.

В этой статье я опишу один из способов реализации приложения, предоставляющего RESTfull API. Вкратце расскажу о том, как я писал подобное приложение на Typescript, а также приведу примеры кода. Существование такой статьи сильно облегчило бы мне жизнь при работе над проектом, надеюсь моя статья поможет и вам!

Немного предыстории

Для тестирования гипотез при развитии продукта требуется в короткие сроки реализовать прототип какого-нибудь приложения. В рамках рабочих задач мне довелось поработать над подобным прототипом. Это было backend-приложение предоставляющее RESTfull API и реализованное с применением технологий Nest.js и Swagger.

Выбор технологий

При выборе стека ключевым требованием стало использование Node.js, так как задача быстрой реализации RESTfull API легла на плечи команды frontend-разработки. При этом в качестве основного инструмента команда обычно использует фреймворк Angular.

Поэтому Nest.js оказался идеальным кандидатом, так как создатели этого фреймворка вдохновлялись подходами, используемыми в Angular. Здесь и привычный нам Dependency Injection, RxJS, Typescript, система модулей и мощный CLI. Полученный API решили задокументировать с помощью Swagger.

Вкратце о технологиях

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

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

Более подробно про Nest рекомендую почитать здесь.

Swagger — это набор инструментов, которые помогают описывать API. Благодаря ему пользователи и машины лучше понимают возможности REST API без доступа к коду. С помощью Swagger можно быстро создать документацию и отправить ее другим разработчикам или клиентам.

Более подробно про Swagger рекомендую почитать здесь.

Подготовка и настройка проекта

Как я уже отметил выше, Nest.js содержит в комплекте довольно мощный CLI. Начнем с его установки, убедившись при этом в том, что у нас на машине установлен Node.js и npm. Для установки выполним команду:

$ npm i -g @nestjs/cli

После того как CLI установлен создадим с его помощью шаблон нашего приложения с именем rest-api-with-swagger:

$ nest new rest-api-with-swagger

В Nest.js сущности, которые отвечают за обработку входящих HTTP-запросов и формирование ответов, называются контроллерами. Ниже приведен пример кода из контроллера (app.controller.ts), созданного по умолчанию при генерации шаблонного проекта:

import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service';  @Controller() export class AppController {   constructor(private readonly appService: AppService) {}    @Get()   getHello(): string {     return this.appService.getHello();   } } 

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

import { Injectable } from '@nestjs/common';  @Injectable() export class AppService {   getHello(): string {     return 'Hello World!';   } }

Сервисы и контроллеры (а также пайпы (Pipes), гарды (Guards) и другие сущности) объединяются в модули (Modules) — строительные блоки, из которых и формируется конечное приложение.

REST API

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

$ nest g resource notes # или nest generate resource notes

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

src notes |-- dto    -- create-note.dto.ts -- update-note.dto.ts |-- entities   -- note.entity.ts -- notes.controller.spec.ts    -- notes.controller.ts   -- notes.service.spec.ts   -- notes.service.ts   -- notes.module.ts ...

Код контроллера, который будет обрабатывать запросы на выполнение операций над заметками, при этом выглядит примерно следующим образом:

import {    Controller, Get, Post,    Body, Patch, Param, Delete, Query  } from '@nestjs/common'; import { NotesService } from './notes.service'; import { CreateNoteDto } from './dto/create-note.dto'; import { UpdateNoteDto } from './dto/update-note.dto';  // Все запросы, содержащие в пути /notes, будут перенаправлены в этот контроллер @Controller('notes') export class NotesController {   constructor(private readonly notesService: NotesService) {}    @Post() // обработает POST http://localhost/notes?userId={userId}   create(   @Query('userId') userId: number, // <--- достанет userId из query строки      @Body() createNoteDto: CreateNoteDto ) {     return this.notesService.create(userId, createNoteDto);   }    @Get() // обработает GET http://localhost/notes?userId={userId}   findAll(@Query('userId') userId: number) {     return this.notesService.findAll(userId);   }    @Get(':noteId') // обработает GET http://localhost/notes/{noteId}   findOne(@Param('noteId') noteId: number) {     return this.notesService.findOne(noteId);   }    @Patch(':noteId') // обработает PATCH http://localhost/notes/{noteId}   update(@Param('noteId') noteId: number, @Body() updateNoteDto: UpdateNoteDto) {     return this.notesService.update(noteId, updateNoteDto);   }    @Delete(':noteId') // обработает DELETE http://localhost/notes/{noteId}   remove(@Param('noteId') noteId: number) {     return this.notesService.remove(noteId);   } }

Вот так вот просто мы получили готовый контроллер, работа которого соответствует всем необходимым правилам построения REST API. Далее, реализуем логику работы с нашими заметками в NotesService. Для упрощения, заметки будем хранить в массиве. В случае реального приложения, в данном сервисе потребовалось бы реализовать логику обращения к сервису работы с хранилищем заметок (например, БД), но это тема другой статьи. Подробнее можно почитать тут.

В первую очередь наполним модели (CreateNoteDto, UpdateNoteDto и Note), описывающие сами заметки и действия над ними. В результате код сервиса будет выглядеть следующим образом:

import { Injectable } from '@nestjs/common'; import { CreateNoteDto } from './dto/create-note.dto'; import { UpdateNoteDto } from './dto/update-note.dto'; import { Note } from './entities/note.entity';  @Injectable() export class NotesService {    private _notes: Note[] = [];    create(userId: number, dto: CreateNoteDto) {     const id = this._getRandomInt();     const note = new Note(id, userId, dto.title, dto.content);     this._notes.push(note);     return note;   }    findAll(userId: number) {     return this._notes.filter(note => note.userId == userId);   }    findOne(noteId: number) {     return this._notes.filter(note => note.id == noteId);   }    update(noteId: number, dto: UpdateNoteDto) {     const index = this._notes.findIndex(note => note.id == noteId)          if (index === -1) {       return;     }      const { id, userId } = this._notes[index];     this._notes[index] = new Note(id, userId, dto.title, dto.content);     return this._notes[index];   }    remove(noteId: number) {     this._notes = this._notes.filter(note => note.id != noteId)   }    private _getRandomInt() {     return Math.floor(Math.random() * 100);   } }

Базовое приложение, реализующее CRUD-операции над заметками (ресурсом), получено. Теперь, перейдем к документированию API. Для этого установим Swagger-модуль для Nest.js:

$ npm install --save @nestjs/swagger swagger-ui-express

и подключим его к нашему приложению в файле main.ts:

import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module';  async function bootstrap() {   const app = await NestFactory.create(AppModule);    const config = new DocumentBuilder()     .setTitle('Notes API')     .setDescription('The notes API description')     .setVersion('1.0')     .build();        const document = SwaggerModule.createDocument(app, config);   SwaggerModule.setup('api', app, document);    await app.listen(3000); }  bootstrap();

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

$ npm run start:dev

После запуска, по адресу http://localhost:3000/api/ отобразится следующая страница:

Страница со Swagger представлением полученного API
Страница со Swagger представлением полученного API

Уже что-то, но на полноценную документацию еще не похоже.

Во-первых, перенесем все методы работы с заметками в отдельную секцию Notes. Для этого повесим очередной декоратор на NotesController:

@ApiTags('Notes')  // <---- Отдельная секция в Swagger для всех методов контроллера @Controller('notes') export class NotesController {   ... }

Также, уберем из нашей документации метод работы с корневым маршрутом (/), повесив на него декоратор ApiExcludeEndpoint:

@Controller() export class AppController {   constructor(private readonly appService: AppService) {}    @Get()   @ApiExcludeEndpoint() // <----- Скрыть метод контроллера в Swagger описании   getHello(): string {     ...   } }

При этом, результат будет выглядеть следующим образом:

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

@ApiTags('Notes') @Controller('notes')  export class NotesController {    ...    @Patch(':noteId') // обработает PATCH http://localhost/notes/{noteId}   @ApiOperation({ summary: "Updates a note with specified id" })   @ApiParam({ name: "noteId", required: true, description: "Note identifier" })   @ApiResponse({ status: HttpStatus.OK, description: "Success", type: Note })   @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "Bad Request" })   update(     @Param('noteId', new ParseIntPipe()) noteId: number,      @Body() updateNoteDto: UpdateNoteDto ) {     return this.notesService.update(noteId, updateNoteDto);   }  ... }

Полный код приложения можно найти в репозитории по ссылке. Список декораторов, позволяющих описать методы API, можно найти по тут. В примере выше для валидации входных параметров метода update используется пайп ParseIntPipe, более подробно с ним и другими встроенными пайпами можно ознакомится по ссылке.

Чтобы корректно сформировать Swagger описание, необходимо модифицировать наши dto, а также класс Note:

import { ApiProperty } from "@nestjs/swagger";  export class Note {     @ApiProperty({ description: "Note identifier", nullable: false })     id: number;      @ApiProperty({ description: "User identifier", nullable: true })     userId: number;          @ApiProperty({ description: "Note title", nullable: true })     title: string;          @ApiProperty({ description: "Note content", nullable: true })     content: string;    ... }

В результате, наше Swagger описание будет выглядеть следующим образом:

API эндпоинты готовы, теперь защитим наш ресурс от несанкционированного доступа. В моем случае, согласно ТЗ требовалось использовать способ доступа с помощью API ключа (подробнее можно почитать здесь). Примеры авторизации с использованием JWT можно посмотреть тут, тут или тут.

Реализовывать авторизацию будем с помощью популярной библиотеки passport, для этого воспользуемся официальным модулем (оберткой) для Nest.js — @nestjs/passport. В первую очередь установим требуемый модуль:

$ npm install --save @nestjs/passport passport passport-headerapikey

Далее, создадим в нашем приложении отдельный модуль, отвечающий за авторизацию:

$ nest g mo authorization # nest generate module authorization

Работа с библиотекой passport основывается на использовании так называемых стратегий авторизации. Реализуем одну из них (api-key.strategy.ts):

import { Injectable, UnauthorizedException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { PassportStrategy } from "@nestjs/passport"; import Strategy from "passport-headerapikey";  @Injectable() export class ApiKeyStrategy extends PassportStrategy(Strategy, "api-key") {      constructor(private readonly _configService: ConfigService) {     super({ header: "X-API-KEY", prefix: "" },            true,            async (apiKey, done) => this.validate(apiKey, done)          );   }    public validate = (incomingApiKey: string, done: (error: Error, data) => Record<string, unknown>) => {     const configApiKey = this._configService.get("apiKey");      if (configApiKey === incomingApiKey) {       done(null, true);     }      done(new UnauthorizedException(), null);   }; }

В примере выше в конструктор ApiKeyStrategy инжектируется сервис ConfigService. Данный сервис является частью пакета @nestjs/config и позволяет упростить работу с файлами, в которых содержатся переменные окружения (т.е. файлы вида .env). В нашем приложении ключ доступа к API является конфигурационным параметром и прописан в файле .env (см. код проекта). Более подробно о работе с модулем конфигурации можно ознакомится тут.

Теперь соберем воедино наш модуль авторизации (authorization.module.ts):

import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { PassportModule } from "@nestjs/passport"; import { ApiKeyStrategy } from "./api-key.strategy";  @Module({   imports: [PassportModule, ConfigModule],   providers: [ApiKeyStrategy], }) export class AuthorizationModule {}

Модуль авторизации готов, но на данный момент методы нашего NotesController продолжат обрабатывать запросы, которые не содержат API ключа в заголовках HTTP-запросов. Для защиты API добавим еще несколько декораторов в контроллер:

@ApiTags('Notes') @ApiSecurity("X-API-KEY", ["X-API-KEY"]) // <----- Авторизация через Swagger  @Controller('notes') export class NotesController {   constructor(private readonly notesService: NotesService) {}    @Post() // обработает POST http://localhost/notes?userId={userId}   @UseGuards(AuthGuard("api-key")) // <---- Вернет 401 (unauthorized)    // при попытке доступа без корректного API ключа ...   create(   @Query('userId', new ParseIntPipe()) userId: number,    @Body() createNoteDto: CreateNoteDto ) {     return this.notesService.create(userId, createNoteDto);   } }

и модифицируем файл main.ts:

async function bootstrap() {   const app = await NestFactory.create(AppModule);    const config = new DocumentBuilder()     .setTitle('Notes API')     .setDescription('The notes API description')     .setVersion('1.0')     .addApiKey({       // <--- Покажет опцию X-API-KEY (apiKey)       type: "apiKey",  // в окне 'Available authorizations' в Swagger       name: "X-API-KEY",        in: "header",        description: "Enter your API key"  }, "X-API-KEY")     .build();      ... }

Конечное Swagger описание нашего API будет выглядеть следующим образом:

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


ссылка на оригинал статьи https://habr.com/ru/post/668340/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *