Введение
Друзья, всем привет!
Меня зовут Алексей и вот уже некоторое время я занимаюсь 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/ отобразится следующая страница:
Уже что-то, но на полноценную документацию еще не похоже.
Во-первых, перенесем все методы работы с заметками в отдельную секцию 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/
Добавить комментарий