Маршрутизация и контроллеры
Теория
URL, URN, URI
Разница между терминами URL (Unified Resource Locator), URN (Unified Resource Name) и URI (Unified Resource Identifier), а также их разделение на составляющие может различаться в зависимости от источника информации. Хотя анатомия URL, URN и URI является не зависящим от фреймворка фундаментальным знанием, необходимо условиться, какая анатомия актуальна конкретно для YDB.
Прежде всего, мы не будем обсуждать разницу между URL и URN, поскольку сейчас важно то, что URI является завершённой сущностью, включающей в себя URL и URN (в зависимости от источника информации, URL и URL могут пересекаться или нет).
Хотя помимо интернета, концепция URI может относиться и к файлам на локальном компьютере, сейчас сконцентрируемся на особенностях URI в веб-разработке.
Анатомия URI
Рассмотрим значимые составляющие URI. Сейчас нас больше интересует то, как мы будем использовать эти составляющие, нежели их канонические определения, поэтому представленный ниже глоссарий содержит описания, а не определения.
- Протокол (Protocol)
- Нас сейчас интересуют протоколы HTTP и HTTPS.
- IP-адрес (IP address)
- В случае локальной разработки чаще всего это локальный IP-адрес (обычно 127.0.0.1, он же localhost). При деплое сайта или приложения на сервер это будет IP-адрес, выданный поставщиком услуг по сдаче серверов в аренду. Мы можем арендовать доменное имя и привязать его к этому IP-адресу, тогда вместо IP-адреса будет это доменное имя.
- Домен (Domain)
- Домен (Domain name)
- Наличие доменного имени востребовано в основном на этапе публикации сайта или приложения. Помимо технологического аспекта, доменное имя является частью брендинга. Тем не менее, разрабатывая сайт или приложение, необходимо принять меры, чтобы переход с одного домена на другой был наиболее простым (в идеале — без внесения изменений в исходный код).
- Порт (Port)
- Благодаря тому что портов существует много, на одном сервере можно запускать несколько сайтов или приложений. Часть портов используется программным обеспечением для разных целей, но сейчас нас интересуют один, максимум 2 порта (когда у нас поддерживаются и HTTP, и HTTPS) для обслуживания HTTP-запросов.
- Сокет (Socket)
- Совокупность домена и порта. Если порт имеет номер по умолчанию (для конкретного протокола, который не является частью сокета), то порт его можно опустить. Например,
https://example.com:443
— то же самое, что иhttps://example.com:443
, покуда протоколом являетсяHTTPS
. - Источник (Origin)
- Совокупность протокола, домена и порта.
- Путь (Path)
- Именно от эта часть наиболее важна для маршрутизации, так как обычно в зависимости от неё выдаются те или иные данные (чаще всего в формате HTML или JSON, если говорить о разработке сайтов и приложений). Звеньев, разделённых косой черной, может быть несколько, однако слишком большое их количество делает маршрутизацию запутанной.
- Параметры поиска (Query parameters)
- Обычно используются для поиска и фильтрации в конкретного набора данных, выдаваемого в зависимости от пути. Однако это лишь общепринятая практика, а как именно будут влиять те или иные параметры поиска на выдаваемые данные — зависит от реализации.
- Хэш (Hash)
- Эта часть обычно не обрабатывается при приёме запроса сервером и имеет значение лишь для клиентской части.
Согласно одному из толкований URL и URN, URL — это то же самое, что и источник, а URN — это часть, начинающая с пути, и, таким образом, соединив URL и URN, мы получим URI. Однако как во многих программных интерфейсах, так и в повседневной жизни термин URL зачастую употребляется как синоним URI либо как его часть, не приведённой выше интерпретации. Например, если взять стандартную функциональность Node.js, то свойство url
объекта request
модуля http
включает в себя путь и параметры запроса, (пример значения из официальной документации: "/status?name=ryan"
), а на основе примера new URL(`https://${process.env.HOST ?? 'localhost'}${request.url}`);
и вовсе нельзя дать ответ, что же такое URL.
Работа с сущностями
Для того, чтобы полноценно рассмотреть маршрутизацию, необходимо понять, для чего она обычно используется. Если рассматривать среднестатистическое серверное приложение, то цель маршрутизации — обеспечить возможность работы с данными. Но «работа с данными» — слишком абстрактно, и если понизить абстрактность, то это будет «работа с объектами» или «работа с сущностями». Опять же, и «объект», и «сущность» являются многозначными терминами, потому очень легко запутаться, но на уровне концепции это — набор информации, соответствующий какому-либо реальному предмету или человеку, например пользователю, товару или статье в блоге. Они могут быть представлены в виде строки в таблице базы данных, ассоциативного массива, объекта в смысле объектно-ориентированного программирования и так далее.
Одна из основных задач, для которой используются фреймворки для бэкенд-разработки — обеспечение возможности удобной манипуляции данными, а если данные с течением времени не меняются, то зачастую можно обойтись без серверного программирования. Основными типами манипуляций с сущностями являются:
- Взятие выборки сущностей, удовлетворяющих определённым условиям (фильтрация)
- Поиск конкретной сущности, которая в чём-либо уникальна
- Создание новых сущностей
- Изменение существующих сущностей
- Удаление сущностей
HTTP-методы
Итак, ответ сервера зависит от того, по какому URI был направлен HTTP-запрос, но если мы говорим о разработке серверной части конкретного сайта или приложения, то источник у нас будет постоянным, а ответ будет зависеть в основном от пути и параметров запроса. Однако есть ещё один фактор, влияющий на ответ сервера, но который не является частью URI — это HTTP-метод. По сути, HTTP-методы — это типы или категории HTTP-запросов, многие из которых не эквивалентны и имеют свои особенности, однако важной их ролью является логическая группировка маршрутов.
Перед тем, как перечислить наиболее популярные HTTP-методы, обратим внимание на несколько моментов, которые часто упускают.
- Названия HTTP-методов являются условными, а конкретный эффект полностью зависит от реализации. Например, HTTP-метод
DELETE
(«удалить») можно реализовать так, что он ничего не будет удалять, а наоборот, что-то создаст. Другое дело, что запутывать маршрутизацию не следует, потому реализация должна соответствовать названию метода. - Маршрутизация будет логичнее и понятнее, если использовать несколько методов, хотя бы 3-4. Тем не менее, это лишь рекомендация, но не техническое требование. До сих пор зачастую обходятся лишь двумя методами — GET и POST, а некоторые начинающие программисты даже не знают, что бывают и другие HTTP методы.
Итак, наиболее популярными HTTP-методами являются:
- GET
- Задуман как метод для получения сущности или коллекции таковых. Кстати, когда мы открываем страницу браузера, то происходит отправка именно GET-запроса, а другие типы HTTP-запросов без использования средств для разработчика или плагинов отправить с помощью популярных браузеров нельзя.
- PUT
- Задуман как метод для создания сущности или полного изменения сущности, но на практике сущность меняется на 100% крайне редко, хотя бы потому что обычно у сущностей есть уникальный идентификатор, не подлежащий изменению за время существования сущности.
- PATCH
- Задуман как метод для частичного изменения сущности.
- POST
- По сути, обобщающий метод по отношению в PUT и PATCH, часто использующийся вместо них.
- DELETE
- Задуман как метод для удаления сущности.
Итак, HTTP-запросы с одним и тем же URI могут давать разный эффект в зависимости от HTTP-метода, например:
- [GET] https://example.com/api/users/1
- Получение данных о пользователе с идентификатором 1
- [PATCH] https://example.com/api/users/1
- Частичное изменение данных о пользователе с идентификатором 1
- [DELETE] https://example.com/api/users/1
- Удаление данных о пользователе с идентификатором 1
Определение маршрутизации и связанных терминов
Теперь, когда мы договорились об анатомии URI, можно ввести понятие маршрутизации (роутинга). Сразу обратим внимание, что:
- Маршрутизация (роутинг) на серверной стороне
- Генерация определённых ответов на HTTP-запросы в зависимости от конкретных URI (в основном от пути и параметров поиска) либо их шаблонов, а также от HTTP-методов, не являющихся частью URI.
- Маршрут
- Комбинация HTTP-метода и URI либо либо его шаблона.
- API серверного приложения
- Реализованный набор маршрутов
Начиная с простейшего примера, рассмотрим маршрутизацию маленького корпоративного сайта. Большинство маршрутов будут иметь тип GET, а ответ будет содержать HTML-код конкретной страницы. Единственное исключение — POST-запрос для отправки формы запроса связи. В общем случае он может ничего возвращать, а лишь просигнализировать, что запрос успешно обработан.
- [GET] https://example.com/
- Главная страница
- [GET] https://example.com/about
- Страница «О компании»
- [GET] https://example.com/services
- Страница «Услуги»
- [GET] https://example.com/access
- Страница «Адрес и схема проезда»
- [GET] https://example.com/contact
- Страница «контакты» с формой запроса обратной связи
- [POST] https://example.com/contact
- Отправка формы запроса обратной связи
Если бы не отправка формы обратной связи, то данный сайт можно было бы реализовать вообще без серверного программирования, а только в виде набора HTML-файлов. Правда для того, чтобы URI не были ссылками на HTML-файлы (например, «https://example.com/about.html»
), а соответствовали приведённым выше маршрутам, потребуется донастроить веб-сервер (пример для nginx), но это проще, чем заниматься веб-программированием. Кроме того, если нужна форма запроса связи без серверного программирования, то можно воспользоваться сторонними сервисами (пример для AWS).
В более же сложном случае у нас будут динамические звенья в путях. На примере интернет-магазина, маршрутизация может быть подобна следующей (некоторые статические маршруты, которые нам уже неинтересны, опущены):
- [GET] https://example.com/
- Главная страница
- [GET] https://example.com/products
- Страница «Список товаров»
- [GET] https://example.com/products/{productID}
- Страница товара, где
{productID}
— идентификатор товара - [POST] https://example.com/products/{productID}/cart
- Добавление товара в корзину
- [DELETE] https://example.com/products/{productID}/cart
- Удаление продукта из корзины
- [GET] https://example.com/checkout
- Страница оформления заказа (так как заказ ещё не оформлен, то идентификатора заказа ещё нет)
- [POST] https://example.com/checkout
- Отправка данных нового заказа (так как заказ ещё не оформлен, то идентификатора заказа ещё нет)
- [GET] https://example.com/vendors/{vendorID}
- Страница поставщика товаров, где
{vendorID}
— идентификатор поставщика - [POST] https://example.com/vendors
- Добавление нового поставщика товаров (только для администраторов)
- [DELETE] https://example.com/vendors/{vendorID}
- Удаление поставщика товаров (только для администраторов)
А что, маршрутизация будет одна на всех пользователей? Как, например, сервер поймёт, в корзину какого пользователя надо добавлять продукт?
Да, одна на всех. Могут быть маршруты, доступные только для конкретной группы пользователей, но маршрут для конкретного пользователя — нечто экзотическое.
А идентификация пользователя — это отдельная и очень большая тема — аутентификация и авторизация. Существуют много подходов в реализации аутентификации и авторизации, но если нужен пример, то данные, идентифицирующие пользователя, могут передаваться через HTTP-заголовки.
Примитивная реализация маршрутизации
Node.js, как впрочем и многие другие языки программирования, не имеет встроенной функциональности для определения маршрутизации и подбора маршрута, соответствующего отправленному HTTP-запросу, а потому маршрутизация — это чуть ли не главная функциональность, которую должен иметь фреймворк для разработки серверной части сайтов и приложений. Но если Вам интересно, какую работу за Вас делает фреймворк, то в общих чертах для реализации маршрутизации необходимо сделать следующее:
- Предложить программный интерфейс и соглашения для определения маршрутов
- При обработке HTTP-запроса, проанализировать его и подобрать походящий маршрут
- Если маршрут содержит динамическую часть (например, идентификатор чего-либо), то необходимо сохранить соответствующие значения так, чтобы к ним был доступ у пользователей фреймворка.
Вот пример реализации маршрутизации на чистом Node.js
(основа на этой статье):
import HTTP from "http";
HTTP.createServer((request: HTTP.IncomingMessage): void => {
const HTTP_Method: string | undefined = request.method;
const currentURL: URL = new URL(request.url ?? "", `http://${ request.headers.host }`);
const pathName: string = currentURL.pathname;
const searchingParameters: URLSearchParams = currentURL.searchParams;
if (HTTP_Method === "GET" && pathName === "/posts" && !searchingParameters.has("id")) {
// GET request to /posts
} else if (HTTP_Method === "GET" && pathName === "/posts" && searchingParameters.has("id")) {
// GET request to /posts?id=123
} else if (HTTP_Method === "POST" && pathName === "/posts") {
// POST request to /posts
}
});
Заметим, что в отличие от предыдущего примера, где идентификатор товара содержался с пути (например, https://example.com/products/1
), здесь идентификатор публикации (по-английски «post», что не следует POST-запросами) передаётся через параметры поиска. Реализация с идентификатором публикации, передаваемой через путь, будет несколько сложнее, особенно она должна работать и для других маршрутов.
Маршрутизация в YDB
Функциональный API
В предыдущих уроках мы определяли единственный маршрут, чтобы получить хоть какую-нибудь обратную связь несмотря на максимальную простоту примеров. Этот маршрут имел HTTP-метод GET и путь /
:
import {
Server,
Request,
Response,
ProtocolDependentDefaultPorts,
HTTP_Methods
} from "@yamato-daiwa/backend";
Server.initializeAndStart({
IP_Address: "127.0.0.1",
HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
routing: [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/",
async handler(request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({ HTML_Content: "<h1>Hello, world!</h1>" }); }
}
]
});
Свойство routing
является массивом, и чтобы определить другие маршруты, нужно аналогичным образом добавить соответствующие элементы:
import {
Server,
Request,
Response,
ProtocolDependentDefaultPorts,
HTTP_Methods
} from "@yamato-daiwa/backend";
Server.initializeAndStart({
IP_Address: "127.0.0.1",
HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
routing: [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Top Page</h1>"
});
}
},
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/products",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Products</h1>"
});
}
},
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/checkout",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Checkout</h1>"
});
}
}
]
});
Как видно, каждый элемент массива routing
в примере выше представляет собой объект с тремя обязательными свойствами — HTTP_Method
, pathTemplate
и handler
. Но почему второе из них называется pathTemplate
— «шаблон пути», а не просто «path» — «путь»? Потому что в общем случае путь может содержать параметры, но поскольку их значения заранее неизвестны, то их необходимо указывать согласно установленным правилам.
Определение маршрутов с параметрами пути
Обнаруживать значения параметров путей в URI (например, идентификатор товара 1
в https://example.com/products/1
) может любой популярный фреймворк для серверной разработки. Однако сделать это с типовой безопасностью могут немногие. Справедливости ради следует отметить, что в случае TypeScript сделать это со стопроцентной типовой безопасностью едва ли возможно — ниже подробнее рассмотрим, почему. Тем не менее, можно не только обойтись без использования ущербного типа any
, но и подкрепить приведение более общих типов к конкретным валидацией.
Чтобы было, с чем сравнить, рассмотрим случай с фреймворком Express.js. Маршрут https://example.com/products/{ID}
там определяется следующим образом:
import type { Express as ExpressApplication } from "express";
import type Express from "express";
import createExpressApplication from "express";
const expressApplication: ExpressApplication = createExpressApplication();
expressApplication.get(
"/products/:ID",
(request: Express.Request<{ ID: string; }>, response: Express.Response): void => {
response.send(`<h1>Product with ID: ${ request.params.ID }</h1>`);
}
);
expressApplication.listen(80, "127.0.0.1");
Как видно, тип Express.Request
является дженериком, первый параметр которого — объект, в котором хранятся извлечённые значения параметров маршрута и который должен удовлетворять привязке { [key: string]: string; }
. Какие здесь проблемы?
- Низкий уровень типовой безопасности
- Приведение типа
{ [key: string]: string; }
к{ ID: string; }
ничем не подкреплено. Несмотря на то, что у нас шаблон пути —"/products/:ID"
, мы можем указать совершенно не имеющий к нему отношения тип (например,{ foo: string; bar: string; }
) и TypeScript ничего не заметит. Фундаментально это проблему решить нельзя, потому что указание типа прекращает своё существование при транспайлинге из TypeScript в JavaScript, соответственно при выполнении JavaScript-кода Node.js-сервером на указанный ранее тип никак нельзя сослаться, чтобы проверить, соответствует ли объектrequest.params
этому типу. Однако должно быть хоть что-то, чем подкреплено приведение{ [key: string]: string; }
к{ ID: string; }
, и в первую очередь это валидация. Сам Express.js такой функциональности не предлагает, но есть сторонние библиотеки, например express-validator. - Неудобный API
- У типа
Express.Request
целых 5 параметров обобщения, причём если нам нужен, например, четвёртый (представленные в виде объекта параметры поиска), то придётся указать и предыдущие три, даже если они для нас не актуальны, потому получится что-то вродеExpress.Request<{}, {}, {}, { pageNumber: number; }>
. typescript-eslint может выразить недовольство таким кодом.
В YDB, обе проблемы имеют решение «из коробки».
Термин «параметр» довольно широкий, и далее пойдёт речь о трёх параметрах в разных смыслах.
- Параметры функции/метода
- Именно этот тип параметров представляется в первую очередь (в программировании), когда произносится слово «параметр». Однако, этот термин используется не только по отношению к функциям и методам.
- Параметры обобщения
- Имеют отношение к языкам программирования со статической типизаций, коим TypeScript хоть и с натяжкой, но является.
- Параметры маршрута
- Имеют отношение к маршрутизации в целом, но не к конкретному языку программирования.
Эта терминология является общей и не связана конкретно с фреймворком YDB, однако программисты с малым опытом могут запутаться.
Сам объект request
(естественно, у него другой тип, не Express.Request
из фреймворка Express.js) параметров обобщения не имеет. Для того, чтобы обратиться к параметрам маршрута, нужно вызывать метод validateAndProcessRoutePathParameters
у объекта request
, при этом:
- Необходимо указать параметр обобщения (ещё раз: не у объекта
request
, а при вызове методаvalidateAndProcessRoutePathParameters
) — представленные в виде единого объекта параметры маршрута. У методаvalidateAndProcessRoutePathParameters
такой параметр обобщения только один. - Через параметром метода нужно передать правила валидации объекта в формате RawObjectDataProcessor из библиотеки @yamato-daiwa/es-extensions. В данном случае мы указываем, что параметр маршрута
PRODUCT_ID
является обязательной (в том смысле, что ниundefined
, ниnull
не допускаются) строкой:
import { Server, Request, Response, ProtocolDependentDefaultPorts, HTTP_Methods } from "@yamato-daiwa/backend";
Server.initializeAndStart({
IP_Address: "127.0.0.1",
HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
routing: [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "products/:PRODUCT_ID",
async handler(request: Request, response: Response): Promise<void> {
const targetProductID: number = request.validateAndProcessRoutePathParameters<{ PRODUCT_ID: number; }>({
PRODUCT_ID: {
preValidationModifications: convertPotentialStringToIntegerIfPossible,
type: Number,
numbersSet: RawObjectDataProcessor.NumbersSets.naturalNumberOrZero,
isUndefinedForbidden: true,
isNullForbidden: true
}
}).PRODUCT_ID;
return response.submitWithSuccess({
HTML_Content: `<h1>Product with ID: ${ targetProductID }</h1>`
});
}
},
// ...
]
});
Кода стало больше. В Express.js было проще и чище.
Поскольку указание правил валидации при вызове метода validateAndProcessRoutePathParameters
является обязательным, то этот метод принуждает программиста подкрепить валидацией приведение к типу, указанному через параметр обобщения. Разумеется, ввиду рассмотренных выше ограничений TypeScript указанные правила валидации могут и не соответствовать типу, указанного через параметр обобщения, однако это лучше, чем просто приведение типов, и, как показывает практика, даже при таких ограничениях вероятность ошибки значительно меньше. В конце урока мы ещё более улучшим код, тем самым ещё снизим вероятность ошибки.
Числовые параметры пути
На первый взгляд очевидно, что 1
в https://example.com/products/1
является числом. Однако большинство фреймворков (причем не только Node.js-фреймворков) по умолчанию извлекают параметры пути маршрута в виде строк (что естественно ввиду того, что URI является строкой, а пытаться преобразовать каждый параметр маршрута у всех URI — это хоть и небольшая, но бесполезная трата вычислительных ресурсов). Таким образом, ранее мы могли при отправке запроса по маршруту /products/:PRODUCT_ID
указать не 1
, а произвольную комбинацию букв и цифр.
Тем не менее, иногда нам действительно нужно, чтобы конкретные параметры пути маршрута были числами. В частности, при хранении сущностей в базах данных зачастую используются числовые ключи, более того, обычно это неотрицательные целочисленные числа (хотя при формировании SQL-запроса всё снова превратится в строку, учитывать числовой тип параметров путей маршрутов всё равно в некоторых случаях нужно, да и потом, не все системы управления базами данных используют SQL). Другими словами, такие URI как http://127.0.0.1/products/1.2
или http://127.0.0.1/products/-3
будут рассматриваться нами как невалидные несмотря на то, что содержат числа.
Хотя в таких фреймворках, как Express.js при определении параметра маршрута можно указать регулярное выражение, пропускающее только цифры, без сторонних библиотек преобразовывать этот параметр к числовому типу данных всё равно придётся:
import type { Express as ExpressApplication } from "express";
import type Express from "express";
import createExpressApplication from "express";
const expressApplication: ExpressApplication = createExpressApplication();
expressApplication.get(
"/products/:ID(\\d+)",
(request: Express.Request<{ ID: string; }>, response: Express.Response): void => {
console.log(typeof request.params.ID);
string
несмотря на то, что идентификатор гарантированно состоит из цифр, иначе этот обработчик маршрута не будет вызван. Прежде чем что-сделать с ним как с числом, придётся привести его к числовому типу — с помощью Number()
или же parseInt
, если нам нужен целочисленный тип. response.send(`<h1>Product with ID: ${ request.params.ID }</h1>`);
}
);
expressApplication.listen(80, "127.0.0.1");
В случае с YDB, как видно из прямого перевода имени метода validateAndProcessRoutePathParameters
(«валидировать и обработать параметры пути маршрута»), данный метод не только валидирует параметры пути маршрута, но и может их обработать, в частности, преобразовать к числовому типу. Это очень просто, но необходимо подробно прокомментировать данную операцию.
- Чтобы выполнить преобразование к числовому типу (ещё раз повторимся, что изначально все параметры пути маршрута имеют строковый тип и их преобразование в числа осуществляется по запросу), нужно воспользоваться функциональностью, которая в API RawObjectDataProcessor называется «предвалидационные преобразования» — по сути, функция либо массив функций, которые принимают на вход параметр типа
unknown
и возвращают его значение либо в изменённом, либо в неизменном виде в зависимости от целей, с которой данная функциональностью используется. В нашем случае строчное значение нужно преобразовать в числовое только если оно содержит валидное число. Для таких случаев есть готовая функцияconvertPotentialStringToNumberIfPossible
. Можно также воспользоваться функциейconvertPotentialStringToIntegerIfPossible
, но тогда если строка будет содержать не целое число, а, например, дробное, то она не будет преобразована в число, и тогда сообщение об ошибке валидации может запутать. - Теперь, когда параметр маршрута преобразован в число (если это было возможно) перед валидацией, указание ожидаемого типа
type: String
необходимо заменить на type: Number. - Согласно API
RawObjectDataProcessor
, если ожидаемым типом является число, то надо указать множество чисел через свойствоnumbersSet
, а также запретNaN
. Если Вас интересуют только положительные целые числа (включая 0), то укажитеRawObjectDataProcessor.NumbersSets.naturalNumberOrZero
, а если же 0 Вы не разрешаете, значит значение будет натуральным числом —RawObjectDataProcessor.NumbersSets.naturalNumber
. NaN разрешать незачем.
import { Server, Request, Response, ProtocolDependentDefaultPorts } from "@yamato-daiwa/backend";
import {
HTTP_Methods,
convertPotentialStringToNumberIfPossible,
RawObjectDataProcessor
} from "@yamato-daiwa/es-extensions";
Server.initializeAndStart({
IP_Address: "127.0.0.1",
HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
routing: [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "products/:PRODUCT_ID",
async handler(request: Request, response: Response): Promise<void> {
const targetProductID: number = request.validateAndProcessRoutePathParameters<{ PRODUCT_ID: number; }>({
PRODUCT_ID: {
preValidationModifications: convertPotentialStringToNumberIfPossible,
type: Number,
numbersSet: RawObjectDataProcessor.NumbersSets.naturalNumberOrZero,
isNaN_Forbidden: true,
isUndefinedForbidden: true,
isNullForbidden: true
}
}).PRODUCT_ID;
return response.submitWithSuccess({
HTML_Content: `<h1>Product with ID: ${ targetProductID }</h1>`
});
}
},
// ...
]
});
Теперь, если мы попытаемся отправить запрос c URI, содержащий нечисловой параметр пути, то получим следующую ошибку:

Можно также попробовать отправить запрос c URI, содержащий параметр пути, представляющий собой отрицательное либо дробное число — от также не пройдёт нашу валидацию, только на сей раз в сообщении об ошибке будет сказано о том, что параметр пути не принадлежит ожидаемому множеству чисел:

Контроллеры
По мере увеличения количества маршрутов естественным образом появится потребность как-то их сгруппировать. Если Вы по какой-то причине стремитесь избежать классов любой ценой, то можно разнести маршруты по файлам и импортировать их в точку входа. Например, у нас два маршрута из уже определённых четырёх связаны с товарами — их мы и вынесем:
import {
Request,
Response,
HTTP_Methods
} from "@yamato-daiwa/backend";
import {
convertPotentialStringToNumberIfPossible,
RawObjectDataProcessor
} from "@yamato-daiwa/es-extensions";
export default [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/products",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Products</h1>"
});
}
},
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "products/:PRODUCT_ID",
async handler(request: Request, response: Response): Promise<void> {
const targetProductID: number = request.validateAndProcessRoutePathParameters<{ PRODUCT_ID: number; }>({
PRODUCT_ID: {
preValidationModifications: convertPotentialStringToNumberIfPossible,
type: Number,
numbersSet: RawObjectDataProcessor.NumbersSets.naturalNumberOrZero,
isNaN_Forbidden: true,
isUndefinedForbidden: true,
isNullForbidden: true
}
}).PRODUCT_ID;
return response.submitWithSuccess({
HTML_Content: `<h1>Product with ID: ${ targetProductID }</h1>`
});
}
}
];
import productRoutes from "./Routes/ProductRoutes";
import { Server, Request, Response, ProtocolDependentDefaultPorts } from "@yamato-daiwa/backend";
import { HTTP_Methods } from "@yamato-daiwa/es-extensions";
/* Running the test:
* ts-node Step4/EntryPoint-Step4.ts
* */
Server.initializeAndStart({
IP_Address: "127.0.0.1",
HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
routing: [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Top Page</h1>"
});
}
},
...productRoutes,
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/checkout",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Checkout</h1>"
});
}
}
]
});
Но, несмотря на то, что глубокое понимание объектно-ориентированного программирования даётся начинающим программистам тяжело, это — мощнейший инструмент с чрезвычайно большим количеством вариантов использования и важнейшее средство обеспечения поддерживамости кода. В данном случае классы будут выполнить роль группировки запросов по какому-либо признаку — такой вариант использования, а точнее шаблон проектирования назваться «контроллер»:
import { Request, Response, Controller } from "@yamato-daiwa/backend";
import {
HTTP_Methods,
convertPotentialStringToIntegerIfPossible,
RawObjectDataProcessor
} from "@yamato-daiwa/es-extensions";
export default class ProductController extends Controller {
@Controller.RouteHandler({
HTTP_Method: HTTP_Methods.get,
pathTemplate: "products"
})
public async generateProductsPage(_request: Request, response: Response): Promise<void> {
routing
, которые мы передавали ранее при инициализации Server.initializeAndStart({ ... })
, только в случае контроллеров свойства handler
нет, потому что обработчиком запроса будет сам декорируемый метод класса. Ещё одно достоинство классов-контроллеров состоит в том, что при качественном именовании методов становится же понятно, что будет сделано при отправке запроса на конкретный маршрут. return response.submitWithSuccess({
HTML_Content: "<h1>Products list</h1>"
});
}
@Controller.RouteHandler({
HTTP_Method: HTTP_Methods.get,
pathTemplate: "products/:PRODUCT_ID"
})
public async generateProductProfilePage(request: Request, response: Response): Promise<void> {
const targetProductID: number = request.validateAndProcessRoutePathParameters<{ PRODUCT_ID: number; }>({
PRODUCT_ID: {
preValidationModifications: convertPotentialStringToIntegerIfPossible,
type: Number,
numbersSet: RawObjectDataProcessor.NumbersSets.naturalNumberOrZero,
isNaN_Forbidden: true,
isUndefinedForbidden: true,
isNullForbidden: true
}
}).PRODUCT_ID;
return response.submitWithSuccess({
HTML_Content: `<h1>Product with ID: ${ targetProductID }</h1>`
});
}
}
import ProductController from "./Controllers/ProductController";
import { Server, Request, Response, ProtocolDependentDefaultPorts } from "@yamato-daiwa/backend";
import { HTTP_Methods } from "@yamato-daiwa/es-extensions";
Server.initializeAndStart({
IP_Address: "127.0.0.1",
HTTP: { port: ProtocolDependentDefaultPorts.HTTP },
routing: [
{
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Top Page</h1>"
});
}
},
ProductController,
routing
. Если повторяющихся маршрутов нет, то можно создавать маршруты как с помощью контроллеров, так и с помощью функций. Заметим также, что создавать экземпляр класса-контроллера не нужно: фреймворк берёт эту рутину на себя. {
HTTP_Method: HTTP_Methods.get,
pathTemplate: "/checkout",
async handler(_request: Request, response: Response): Promise<void> {
return response.submitWithSuccess({
HTML_Content: "<h1>Checkout</h1>"
});
}
}
]
});