[Разработка веб-сайтов, JavaScript, Node.JS, API, TypeScript] Декларативный API на Next.JS — реальность?
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет! Меня зовут Андрей, я 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
===========
Похожие новости:
- [Научно-популярное] В Денисовой пещере на Алтае найдены следы Homo sapiens
- [Node.JS, ReactJS] Splunk-react-app или создание дашбордов любой сложности в Splunk
- [JavaScript, Node.JS, TypeScript] Dynamic modules в NestJS
- [Разработка веб-сайтов, CSS, HTML] Дизайнерский Multiselect на протеинах
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] React испортил веб-разработку (перевод)
- [Научно-популярное] Антропологи обнаружили в Израиле неопределённый вид Homo
- [Разработка веб-сайтов, Работа с видео, Программирование, Видеоконференцсвязь] How to make a multipoint WebRTC conference (MCU) with recording and screen-sharing in the browser
- [Разработка веб-сайтов, Работа с видео, Программирование, Видеоконференцсвязь] Как сделать многоточечную WebRTC-конференцию (MCU) с записью и демонстрацией экрана в браузере
- [Веб-дизайн, Разработка веб-сайтов, JavaScript, Программирование] Лёгкая, гибкая, производительная обёртка над Web Animations API — @okikio/animate (перевод)
- [JavaScript, Node.JS, DevOps] Непрерывная интеграция и развертывание (Deployment) с помощью Jenkins (перевод)
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_node.js, #_api, #_typescript, #_nextjs, #_nestjs, #_dekoratory (декораторы), #_deklarativnoe_programmirovanie (декларативное программирование), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_node.js, #_api, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:03
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет! Меня зовут Андрей, я 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); } // 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 }; } } import { IsNotEmpty, IsEmail } from 'class-validator';
export class CreateUserDTO { @IsEmail() email: string; @IsNotEmpty() fullName: string; } @Get('/users')
@Query('isActive', ParseBooleanPipe({ nullable: true })) isActive?: boolean @Get('/users/:userId')
@Param('userId', ParseNumberPipe) userId: string, 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'; } } @UseMiddleware(() => ...)
class User { // ... 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 =========== Похожие новости:
Разработка веб-сайтов ), #_javascript, #_node.js, #_api, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:03
Часовой пояс: UTC + 5