Написание слоя API в приложении — это прошлый век! Встречайте универсальный прокси

от автора

Большинство компаний регулярно сталкиваются с проблемой постоянной модификации слоя API в своих веб-приложениях в ответ на изменения API сервера. Некоторые из них прибегают к автогенерации кода на основе схем Swagger, другие переписывают код вручную. Однако эти подходы требуют значительных ресурсов и времени, а также часто приводят к избыточному коду, который растет в объеме вместе с количеством задействованных в коде методов API, увеличивая размер бандловI.

В данной статье я хочу поделиться методом, который позволяет избежать этих сложностей.

Давайте начнем с определения сути и назначения API слоя в веб-приложениях. Этот слой представляет собой интерфейс между приложением и бэкэндом, его основные задачи:

  • предоставление списка доступных методов API и их описание

  • выполнение запроса к серверу и возврат ответа сервера приложению

Этот слой не должен содержать логику, кроме логики транспортировки данных между фронтендом и бэкэндом. В нем не должно быть никаких дополнительных вычислений или обработки данных — это задача ядра приложения.

Если мы взглянем на ситуацию объективно, мы увидим, что для разработчика изменяются только семантика вызовов методов и их количество в API — остальные аспекты не так важны.

Вопрос, который мучает многих разработчиков: «Почему мне приходится возиться с этим прокси-слоем каждый раз, когда меняется API на бэкэнде?»
Ответ, возможно, уже скрывается в самом вопросе.

Идея очень проста и в то же время гениальна: прокси-объект + TypeScript!

Прокси-объект позволяет нам делать практически все, что мы захотим, а TypeScript не даст нам сделать лишнего — то, чего нет в интерфейсе API!

Свобода действий при ограничениях — это ключ к гармонии!

Прокси-объект позволяет нам «схлопнуть» весь тот однотипный код десятков, сотен методов в слое АПИ, который генерируют инструменты или пишет человек (код по передаче и получению данных) в один метод.

Проще говоря: прокси конструирует и вызывает метод во время его вызова из кода приложения.

Давайте я вам покажу, как это работает на примере:

const getAPI = (apiUrl) =>     new Proxy(         {},         {             get(_, method_name) {                 return async (props) => {                     const apiMethod = camelToSnake(method_name);                     const httpMethod = apiMethod.split('_')[0].toUpperCase();                     const isGetMethod = httpMethod === 'GET';                     const url = new URL(`${apiUrl}/${apiMethod}`);                     const options = {                         method: httpMethod,                         headers: { 'Content-Type': 'application/json' },                     };                      if (isGetMethod) {                         url.search = new URLSearchParams(props).toString();                     } else {                         options.body = JSON.stringify(props);                     }                      const response = await fetch(url, options);                     return response.json();                 };             },         },     ); 

В коде для примера представлена упрощенная реализация логики прокси-объекта API. Допустим мы предполагаем, что имена методов API всегда начинаются с названия HTTP-метода, а реальные имена методов на бэкэнде имеют snake формат записи ( на практике у вас могут быть любые другие условия и соглашения ).

Пример использования прокси-объекта:

const api = getAPI('http://localhost:3000/api');  // С Proxy мы можем писать реальные вызовы методов api.getTodos();  // --> fetch('http://localhost:3000/api/get_todos?...', { method: 'GET', ... })  api.postTodo({ title: 'test' });  // --> fetch('http://localhost:3000/api/post_todo', { method: 'POST', ... }))  api.deleteTodo({ id: 1 });  // --> fetch('http://localhost:3000/api/delete_todo', { method: 'DELETE', ... }))  // С Proxy никто не запретит нам писать всякую билеберду api.putLalalalala('lololololo');  // --> fetch('http://localhost:3000/api/put_lalalalala', { method: 'PUT', ... })) 

Однако, чтобы ограничить нашу фантазию рамками реальной реализации API, нам необходимо описание API в виде интерфейса на TypeScript.

type Todo = {     id: number;     title: string; };  interface API {     getTodos: async () => Todo[];     postTodo: async ({ title: string }) => Todo;     deleteTodo: async ({ id: number }) => Todo; } 

Давайте дополним реализацию прокси-объекта типами:

import type { API } from '@companyName/api';  const getAPI = (apiUrl) => new Proxy( ... ) as API; 

Теперь api будет содержать описания методов API из интерфейса TypeScript, что обеспечивает автоподсказки в IDE и не позволяет нам выходить за рамки реальной реализации. Typescript предоставляет нам описания методов API, производит валидацию параметров и обеспечивает информацией о возвращаемом результате.

Попытка написания вызова не существующего метода:

api.putLalalalala('lololololo'); 

или любого другого вызова, не соответствующего интерфейсу, приведет к ошибке.

Если на сервере изменится интерфейс API, разработчики бэкэнда должны будут изменить описание интерфейса (сгенерировать новую версию при помощи инструментов для swagger или изменить руками — для тех кто АПИ пишет руками) и опубликовать его новую версию. Веб-приложению при этом лишь нужно обновить пакет с описанием интерфейса.

Данный подход предполагает, что API имеет строгую систему: ограничена одним протоколом и имеет четкие ограничения, которые можно описать и реализовать в proxy-объекте. Если никакой системы в проектируемом API нет — данный подход ничем ему уже не поможет.

В нашем проекте используется протокол JSON RPC — протокол предназначенный для общения всех внутренних сервисов компаний. Данный подход наилучшим способом ложится на его имплементацию и позволяет реализовать практически любые вызовы с любыми параметрами.

Заключение

Применение связки прокси-объекта и TypeScript для реализации API слоя позволяет существенно упростить процесс разработки и поддержки приложения, избегая постоянного переписывания или генерации кода слоя API в веб-приложении при изменении API на бэкэнде.

Размер вашего слоя API будет постоянно минимальным вне зависимости используете вы десятки или десятки тысячи методов API в коде (сравните это с постоянно растущим размером текущие реализации API на базе сгенерированного или написанного вручную кода).

Более того — данный код может быть единым для многих веб приложений, Его можно оформить в отдельный пакет и использовать во множестве проектов компании.

Способ очень простой, легковесный и требует лишь периодического обновления типов, описывающих интерфейс API.

Проекты, которые использовали кодогенерацию из схем OpenAPI могут для данного подхода генерировать интерфейс API на typescript, благо таких инструментов достаточно.

Перестаньте писать и генерировать и переписывать API слой на каждое изменение сервера API, не тратьте деньги инвесторов и владельцев бизнеса на нагрев планеты — это давно уже не модно !

Демо проект: https://github.com/budarin/proxy-api-demo

ЗЫ: из комментариев стало известно — мое решение не единичное — в промышленной разработке аналогичный подход с Proxy используется в одной из ERP систем и думаю еще не в одной (там ребята уж точно знают толк и цену сэкономленному объему кода, ресурсам и времени), так что если вы не разобрались в сути и/или боитесь выйти из зоны комфорта делая «как все» — эта информация придаст вам немного смелости ?

В начале новая идея вызывает отрицание от не понимания ее сути или из-за того, что это выводит вас из зоны комфорта: «по этой теме еще нет хайпа, все вокруг генерируют бесполезный код, мы тоже наладили такой же как у всех процесс и тратим в год миллионы на ветер, как и большинство, и тысячи часов на генерацию однотипного бесполезного кода, тратим время и деньги на передачу всего этого бесполезного кода по сети и нам это уже привычно — мы не хотим отказываться от этой привычки». Но с осознанием идеи и ее пользы — приходит принятие.

Смелее! Скоро такой подход будет на подавляющем большинстве проектов — он экономит и деньги и время разработчиков!


ссылка на оригинал статьи https://habr.com/ru/articles/798887/