[Разработка веб-сайтов, JavaScript, Node.JS, API, TypeScript] Декларативный API на Next.JS — реальность?

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
27-Июн-2021 21:32

Привет! Меня зовут Андрей, я 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: позволяет определить необходимо ли дальнейшее исполнение кода в обработчике или же стоит прервать выполнение досрочно). Например, проверка авторизации посредством 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. Пакет экспортирует небольшой набор заранее подготовленных исключений, что позволяет не указывать код ответа вручную, а лишь прописывать необходимые ошибки в ответе сервера. Доступны следующие исключения:Статус кодСообщение по умолчаниюBadRequestException400'Bad Request'UnauthorizedException401'Unauthorized'NotFoundException404'Not Found'PayloadTooLargeException413'Payload Too Large'UnprocessableEntityException422'Unprocessable Entity'InternalServerErrorException500'Internal Server Error'Имеется даже возможность объявлять свои собственные обработчики ошибок на уровне эндпоинта с использованием декоратора 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';
  }
}
Весь этот набор инструментов позволяет описывать эндпоинты максимально декларативно и разбивать код на более мелкие компоненты, а это в свою очередь значительно упрощает юнит-тестирование и отладку, что критически важно для сохранения психического здоровья и времени разработчика :)Ознакомиться с документацией можноздесь, посмотреть исходники можно тут. Спасибо что дочитали до конца и не растеряли интерес по пути!
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_node.js, #_api, #_typescript, #_nextjs, #_nestjs, #_dekoratory (декораторы), #_deklarativnoe_programmirovanie (декларативное программирование), #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_node.js, #_api, #_typescript
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 19-Май 09:04
Часовой пояс: UTC + 5