Привет! Меня зовут Андрей, я Backend Node.JS разработчик в одной из зарубежных компаний, занимающихся разработкой системы для администрирования офисов. Наше приложение и его веб-версия предоставляют арендодателям возможность отслеживать заполненность офиса, обеспечивать подключение IoT-устройств для отслеживания, например, количества еды в холодильниках или остатка воды в кулерах, выдавать пропуски для сотрудников в своё здание и много чего другого. Одним из важнейших узлов в этой системе является API как для внутренних пользователей, использующих приложение или веб-сайт, так и для клиентов, использующих наше Whitelabel решение. Всего в нашей системе зарегистрировано более двух сотен API эндпоинтов, для построения которых мы использовали фреймворк NestJS. Если вы по какой-то причине ещё не слышали про Nest, то я настоятельно рекомендую ознакомиться со статьёй NestJS - тот самый, настоящий бэкенд на nodejs. Одной из основных и наиболее значимых особенностей NestJS является нативная поддержка декораторов, что в свою очередь позволяет создавать эндпоинты декларативно.
@Get('/v2/:key') @HttpCode(HttpStatus.OK) async getContentFromCacheByKey( @Param('key') key: string, ): Promise<GenericRedisResponse> { const result = await this.cacheService.get(key); if (!result) { throw new NotFoundException(`There is no key ${key} in cache`); } return result; }
Особенно польза декораторов становится заметна когда возникает необходимость принимать различные типы запросов по одному и тому же пути. Например, когда нам необходимо не только «брать» данные по ключу из кэша, но и сохранять данные под нужным нам ключом. Путь остаётся прежним, меняется лишь декоратор и содержимое метода.
@Post('/v2/:key') @HttpCode(HttpStatus.NO_CONTENT) async getContentFromCacheByKey( @Param('key') key: string, @Body() body: GenericRedisBody, ): Promise<void> { await this.cacheService.set(key, body.data, body.ex, body.validFor); }
Это очень удобно хотя бы потому, что отпадает необходимость создавать витиеватые методы с запутанными условными операторами. Не говоря уже об удобстве юнит-тестирования.
Несмотря на то, что днём я разрабатываю на NestJS, ночью я трансформируюсь в ярого фаната NextJS и стараюсь переписать на нём горсть из своих pet-проектов. К сожалению, в NextJS не реализована нативная поддержка декораторов для API, однако недавно с удивлением для себя я обнаружил, что кто-то пытается привнести это новшество в NextJS и это именно то, о чём я собираюсь сегодня рассказать.
@storyofams/next-api-decorators
Добавляет поддержку декораторов для API routes в NextJS. Написан на TypeScript, покрыт тестами на 100%, но ещё совсем молод и не очень популярен. Этой статьёй я попытаюсь исправить ситуацию и помочь людям поближе познакомиться с декларативным подходом к написанию API эндпоинтов в NextJS. Начать предлагаю с рассмотрения простого набора эндпоинтов для манипуляции с пользователями:
// pages/api/user.ts class User { // GET /api/user @Get() async fetchUser(@Query('id') id: string) { const user = await DB.findUserById(id); if (!user) { throw new NotFoundException('User not found.'); } return user; } // POST /api/user @Post() @HttpCode(201) async createUser(@Body(ValidationPipe) body: CreateUserDto) { return await DB.createUser(body.email); } } export default createHandler(User);
Для сравнения, всё то же самое, но императивно:
export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { const user = await DB.findUserById(req.query.id); if (!user) { return res.status(404).json({ statusCode: 404, message: 'User not found' }) } return res.json(user); } else if (req.method === 'POST') { // Very primitive e-mail address validation. if (!req.body.email || (req.body.email && !req.body.email.includes('@'))) { return res.status(400).json({ statusCode: 400, message: 'Invalid e-mail address.' }) } const user = await DB.createUser(req.body.email); return res.status(201).json(user); } res.status(404).json({ statusCode: 404, message: 'Not Found' }); }
На этом функциональность не заканчивается. Вы можете использовать декораторы для установления определённых заголовков как для единичных обработчиков, так и для всего набора в классе:
@SetHeader('Content-Type', 'text/plain') class UserHandler { @Get() users(@Header('Referer') referer: string) { return `Your referer is ${referer}`; } @Get('/json') @SetHeader('Content-Type', 'application/json') users(@Header('Referer') referer: string) { return { referer }; } }
Более того, добавляется поддержка валидации для полей тела запроса! Точь в точь как в NestJS. Для этого вам необходимо установить пакет class-validator и описать class members с использованием декораторов:
import { IsNotEmpty, IsEmail } from 'class-validator'; export class CreateUserDTO { @IsEmail() email: string; @IsNotEmpty() fullName: string; }
В случае когда как минимум одно из полей не проходит валидацию, ваш эндпоинт вернёт ошибку 422 Unprocessable Entity. При этом валидировать можно не только тело запроса, но и параметры и query элементы:
@Get('/users') @Query('isActive', ParseBooleanPipe({ nullable: true })) isActive?: boolean
В данном случае позволяется опускание аргумента isActive, однако если он в URL присутствует, то он обязательно должен быть типа boolean. Это же применимо и к параметрам:
@Get('/users/:userId') @Param('userId', ParseNumberPipe) userId: string,
Помимо валидации параметров и аргументов возможно так же проводить различного рода проверки в middleware, и применимо это как к единичным обработчикам, так и ко всему набору обработчиков. NestJS разработчики знакомы с таким понятием, как Guards (TLDR: позволяет определить необходимо ли дальнейшее исполнение кода в обработчике или же стоит прервать выполнение досрочно). Например, guard, обеспечивающий проверку валидности JWT-токена ещё до того, как будет запущен код в самом эндпоинте:
const JwtAuthGuard = createMiddlewareDecorator( (req: NextApiRequest, res: NextApiResponse, next: NextFunction) => { if (!validateJwt(req)) { throw new UnauthorizedException(); // или return next(new UnauthorizedException()); } next(); } ); class SecureHandler { @Get() @JwtAuthGuard() // здесь используется объявленный ранее обработчик public securedData(): string { return 'Secret data'; } }
Для использования middleware для всего набора обработчиков используется декоратор useMiddleware:
@UseMiddleware(() => ...) class User { // ...
Внимательные читатели могли заметить, что в одном из примеров ранее используется кастомное исключение UnauthorizedException. Пакет экспортирует небольшой набор заранее подготовленных исключений, что позволяет не указывать код ответа вручную, а лишь прописывать необходимые ошибки в ответе сервера. Доступны следующие исключения:
|
Статус код |
Сообщение по умолчанию |
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Имеется даже возможность объявлять свои собственные обработчики ошибок на уровне эндпоинта с использованием декоратора Catch. Полезно когда вы знаете о заведомой нестабильности эндпоинта (например, внешний API-сервис иногда вываливается в 503 ошибку и вам необходимо подготовить определённую структуру ответа вместо вывода встроенного исключения):
import { Catch } from '@storyofams/next-api-decorators'; function exceptionHandler( error: unknown, req: NextApiRequest, res: NextApiResponse ) { const message = error instanceof Error ? error.message : 'An unknown error occurred.'; res.status(200).json({ success: false, error: message }); } @Catch(exceptionHandler) class Events { @Get() public events() { return 'Our events'; } }
Весь этот набор инструментов позволяет описывать эндпоинты максимально декларативно и разбивать код на более мелкие компоненты, а это в свою очередь значительно упрощает юнит-тестирование и отладку, что критически важно для сохранения психического здоровья и времени разработчика 🙂
Ознакомиться с документацией можно здесь, посмотреть исходники можно тут. Спасибо что дочитали до конца и не растеряли интерес по пути!
ссылка на оригинал статьи https://habr.com/ru/post/564942/
Добавить комментарий