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

737f75af51f221472faec008a853dedd

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

Давайте начнем с определения сути и назначения 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, разработчики бэкэнда должны будут изменить описание интерфейса и опубликовать его новую версию. Веб-приложению при этом лишь нужно обновить пакет с описанием интерфейса.

Заключение

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

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

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

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

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

© Habrahabr.ru