Как избавиться от постоянной кодогенерации 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(methodName);
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, разработчики бэкэнда должны будут изменить описание интерфейса и опубликовать его новую версию. Веб-приложению при этом лишь нужно обновить пакет с описанием интерфейса.
Данный подход предполагает, что API имеет строгую систему: ограничена одним протоколом и имеет четкие ограничения, которые можно описать и реализовать в proxy-объекте. Если никакой системы в проектируемом API нет - данный подход ничем ему уже не поможет.
Заключение
Применение связки прокси-объекта и TypeScript для реализации API слоя позволяет существенно упростить процесс разработки и поддержки приложения, избегая постоянного переписывания или генерации кода слоя API в веб-приложении при изменении API на бэкэнде.
Размер вашего слоя API будет постоянно минимальным вне зависимости используете вы десятки или десятки тысячи методов API в коде (сравните это с постоянно растущим размером текущие реализации API на базе сгенерированного или написанного вручную кода).
Более того - данный код может быть единым для многих веб приложений, Его можно оформить в отдельный пакет и использовать во множестве проектов компании.
Способ очень простой, легковесный и требует лишь периодического обновления типов, описывающих интерфейс API.
Проекты, которые использовали кодогенерацию из схем OpenAPI могут для данного подхода генерировать интерфейс API на typescript, благо таких инструментов достаточно.
Перестаньте писать и генерировать и переписывать API слой на каждое изменение сервера API - это давно уже не модно!
Демо проект: https://github.com/budarin/proxy-api-demo
Top comments (0)