Как стать автором
Обновить

Комментарии 134

Похоже на крик души и немного сумбурно вышло из за этого, но суть верна, используйте интерфейс в отрыве от реализации, а потом хоть mock повесьте для тестирования, хоть разные реализации интерфейса используйте. Единственное что я бы хотел добавить это дробление интерфейсов на логичные модули и не забывать что сервер не всегда возвращает ответ, а может и ошибку вернуть, что в рантайме даст спецэффекты.

Наверное в статье я не достаточно четко написал, что реализуется один простой вариант для примера. Я специально не стал писать полные реализации для того чтобы разработчики смогли понять суть и не погрязли бы в деталях реализации.
У нас в коде реализован JSON RPC в котором все есть и данные и ошибки.

Большинство компаний регулярно сталкиваются с проблемой постоянной
модификации слоя API в своих веб-приложениях в ответ на изменения API
сервера

Если честно, я не очень понял, как этот метод спасает от изменения API на бэке. Если часть каких-то данных переместили в другой эндпоинт? Или разбили логику на несколько эндпоинтов? Или поменялся content type? А как это работает с вложенными эндпоинтами, напр., item/{id}/nested_item ? Для чего писать в начале методы, если у нас уже есть соглашение в рамках RESTfull API?

Возможно мое выражение "API слой приложения" не достаточно точное, но в данной статье речь шла только о слое API в приложении - бэк не рассматривался

Ваше же приложение с бэком взаимодействует по этому API. Как ваш подход спасает от переписывания приложения, если не бэке что-то поменяется?
Да и на остальные вопросы из ветки хотелось бы услышать ответ.
На todo-приложении понятно, что оно выглядит красиво, а как такой подход в чем-то более приближенном к реальности выглядит?

Давайте начнем сначала:
- Proxy позволяет нам писать любой вызов, которого в реальности нет у объекта
- интерфейс на typescript позволяет нам генерировать ошибку для не существующих вызовов, не соответствующих описанию вызовов или изменившихся на сервере

При изменении API на сервере вы обновляете описание интерфейса API на typescript и для изменившихся методов в коде получаете ошибки. Вы просто меняете в коде приложения методы вызова с учётом изменений - всё! вам не нужно менять код слоя API - Proxy объект за вас сам применит все изменения

Задавайте вопросы - отвечу на остальные

Переизобретение позднего связывания. Собственно тот случай когда свои шишки набивать нету необходимости - достоинства и недостатки позднего связывания, особенно как в данном случае "на стероидах" - давно набиты.

Hint: начать можно с простого - ошибка в написании имени метода выявляется только при запуске приложения. В то время как уже практически доказано что shift left подход (обнаруживать ошибки как можно раньше) - дает видимое увеличение качества и при ускорении time to market - зачем-то делается shift to right. Дальше можно поразвивать эту мысль, например в сторону что произойдет со статическим анализом что качества, что безопасности кода.

Мотивация понятна - предложенный путь дает небольшую экономию времени разработчика (но не времени до вывода на прод). За это кстати надо тоже сказать спасибо метрикам типа velocity которые внедряют псевдо-скрам-мастера, или еще хуже - утилизации (вообще коробит когда про людей говорим) вместо бизнес-ориентированных метрик того же Evidence Management Guide.

Так что чисто технически - действительно интересная работа, но прикладной смысл далеко не очевиден.

посмотрите демо проект

Кстати, у вас там перепутаны POST и PUT.

вполне возможно - посмотрю

перепроверил у ИИ - нет, всё вроде бы верно

В протоколе REST для создания сущности обычно используется метод HTTP POST, а для обновления существующей сущности — метод PUT или PATCH.

  • POST используется для создания новой сущности в коллекции ресурсов.

  • PUT используется для полного обновления существующей сущности или создания новой по определенному URI, если она еще не существует.

  • PATCH применяется для частичного обновления существующей сущности.

да, спасибо за подсказку - нашел в маппинге было не верно - теорию то точно знаю, но писал за один вечер быстро и переставил местами не нарочно :)

>Если часть каких-то данных переместили в другой эндпоинт?
перемещаете данные из одного эндпоинта в другой в коде приложения

>А как это работает с вложенными эндпоинтами, напр., item/{id}/nested_item ? 
Что вы подразумеваете под вложенными эндпоинтами? Если вы имеете в виду иерархические структуры данных - просто описываете тип на typescript и он проконтролирует чтобы вы положили туда именно то что требуется

>Или поменялся content type?
меняете content-type в вызове метода - я же реализацию написал для примера - вы должны написать реализацию под ваши условия и ограничения

>если у нас уже есть соглашение в рамках RESTfull API
в коде показан пример относящийся к REST

Большинство компаний регулярно сталкиваются с проблемой постоянной
модификации слоя API в своих веб-приложениях в ответ на изменения API
сервера

Получается, что вы меняете слой API практически также, как и в реализации без прокси. Также, был упомянут вариант с кодогенерацией по спеке openapi, в котором как раз вообще ничего не меняется в слое API (ну как, пока генератор нормально генерирует, потому что там тоже даже для популярнейшего тайпскрипта периодически всплывают баги даже в самых популярных генераторах).В общем, я не понял, чем ваш вариант лучше существующих и решает ли он вообще проблему.

нет вы не правильно поняли - слой API в приложении пишется один раз и больше никогда не изменяется. Он в соответствии с вашими соглашениями и ограничениями будет генерировать любые вызовы, которые вы попытаетесь вызвать на нем (объект Proxy в js позволяет для такого объекта делать вызовы не существующих методов - этот объект перехватывает эти вызовы и выполняет определенный код для них, который вы напишете).

В это же время typescript не позволит вам делать вызовы которых нет в интерфейсе API

При генерации API слоя или написания его руками - вы тратите ресурсы и время на них, слой API в приложении растет

В предложенном мною методе - вы просто на сервере из кода извлекаете типы и просто делитесь ими с фронтом - при изменениях в API при обновлении типов в коде могут возникнуть ошибки связанные с изменением вызовов API которые нужно исправить в приложении и всё ничего больше исправлять и изменять не нужно

Идея отличная,

Но будет ли такое работать в реальных проектах?

у нас на проекте все работает и горя не знаем

А зачем мне какая-то прокладка typescript, если можно просто реализовать условно метод, который принимает действие в качестве входной переменной и просто переводит это в формат:

http://localhost/[действие]/… +[массив пост/гет]

А руление делается обычным проксированием nginx куда в том числе, если нужно, можно отработать как защиту, так и прямое проксирование на микросервис или эндпоинт виде монолита или микросервиса?

С изменением API вам его придется изменять - Proxy позволяет один раз написать реализацию ваших соглашений и требований к названиям методов, методам HTTP и возвращаемым результатам и не изменять этот слой руками больше никогда - меняются только типы

Хм, мы видимо друг друга не понимаем. С изменением API я никогда ничего не дописываю, кроме как обновляю документацию при изменении воркера который обслуживает тот или иной энд поинт.

Метод вызова тоже не обновлялся уже наверное года 2-3.

да действительно, я скорее всего не понял как у вас реализовано

я понимаю, что можно писать без typescript, но считаю что это не верно - типизация это и контракт и документация и банальная гарантия от очепяток и ошибок.

я знаю что много разработчиков не любят typescript, но в промышленной разработке к сожалению без него - никак

Напоминает RPC. Единая точка входа, маппинг на любые методы и DTO.

ИМХО, все (ну пусть не все, но многие) начинают делать REST, а в конце-концов получается RPC.

Нужно использовать протоколы по их назначению - REST предназначен для публичных API и таки да он сделан удобным для публики, но не для разработчиков
Но многие используют REST для внутреннего общения сервисов и больно мучаются при этом

Я бы еще добавил, что REST хорошо подходит для CRUD, что-то немного более сложное на нем делать неудобно.

хорошо реализованный рест упрощает некоторые сценарии, например обработку ошибок. Если правильно выделить сущности и наладить идемпотентность там где она нужна, то можно безопасно повторять запросы. С json-rpc такой фичи нету.

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

Согласен. Если API написано без применения системы - да к сожалению такой подход не подойдет и придется все время генерировать либо код по схемам либо писать его вручную.
Мы в проекте используем JSON RPC, который позволяет описать практически любую реализацию метода в его рамках и он отлично реализуется предложенным методом.

В odoo erp именно так все и реализовано по умолчанию, по стандарту json rpc v2. Ничего не нужно придумывать.

в веб проектах erp системы не используются

я о такой erp системе даже и не слышал так что уж извините - идея ко мне пришла когда рассматривая кучу всяких библиотек с Proxy.У нас так же реализовано все это на json rpc который мы используем уже долгие годы

А за информацию - спасибо! Это дополнительно подтверждает, что мое решение верное!

Идея хороша, но допиливать надо будет напильником по месту. Универсальности нет

универсальность есть если у вас есть системное видение в апи - я привел пример системы для реализации REST протокола. Мы используем JSON RPC - отлично все легло без напильника :)

Типа вынести работу со сторонними API в отдельную библиотеку и при изменении API править только эту библиотеку, а не весь проект, я правильно понимаю?

нет - немного не так!

Если у вас API имеет какую-либо систему (у нас на проекте это JSON RPC, у вас может быть своя система к примеру как в коде статьи) - один раз пишите реализацию API слоя, используя Proxy, реализуете все соглашения и ограничения, оформляете в пакет и используете на всех проектах, которые используют такие же соглашения в API.

В проектах при изменении API на бэке обновляете только описание интерфейсов API на typescript, не переписывая сам код API слоя (он не меняется никогда)

И чем это отличается от библиотеки в которую вынесены функции работы с внешним API ?

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

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

То есть, отличие в том, что тут править не библиотеку, а typescript ?

нет - ни то ни другое :)

в коде приложения (не в слое API) вам придется изменять только те вызовы что изменились в API: добавить параметр к вызову, переименовать метод, удалить удаленный вызов и т.д.

вы обновляете интерфейс на typescript - он в соответствии с изменениями генерирует ошибки в тех местах кода приложения где API изменился - вы просто исправляете эти ошибки в коде приложения - всё! на клиенте больше ничего не нужно изменять! Proxy подхватит ваши изменения в вызовах и выполнит вызовы API так как вам нужно

изменения в typescript делают ребята, которые пишут сервер API - после изменений они публикуют пакет с описанием интерфейса на typescript и все приложения, которые используют данное API обновляют пакет в своих приложениях и делают только правки в коде приложения там где API изменилось

Ненене. Вот изменилось API на беке (пока оставим в стороне версионность) - с чего вдруг "изменения в typescript делают ребята, которые пишут сервер API "?
Они передали фронту новый контракт, а что фронт с этим делает - их не волнует

А кто же кроме ребя на сервере знает что изменилось???
Кто меняет - тот и пишет изменения! :)

После изменения кода API они меняют и публикуют новый контракт в виде интерфейса на typescript - все просто!

Если ребята наши (JavaScript) то пишут то они как проффи на typescript - им и делать то ничего не нужно - просто извлечь описание типов из кода при помощи tsc и опубликовать

Ага, давай те еще скажите что и техдокументацию они же должны писать. (что желательно, но не обязательно).

Вот это не понял "они меняют и публикуют новый контракт в виде интерфейса на typescript - " - как вы это представляете себе?

Вот добавили в апи новый метод - что дальше? Кто что должен делать чтобы все не сломалось?

Вот это не понял "они меняют и публикуют новый контракт в виде интерфейса на typescript - " - как вы это представляете себе?

после добавления метода на typescript описание метода будет автоматически добавлено в описание интерфейса API - новую версию извлекают при помощи tsc и публикуют.

Если код пишется не на typescript - руками ли либо при помощи тулзы генерируется новое описание API на typescript (в нем будет описание нового метода) Затем контракт так же публикуется

Затем разработчики приложения обновляют у себя пакет и в коде находят ошибки в тех местах где API изменилось - правят код вызова в соответствии с подсказкой IDE о типах и интерфейсе из typescript и всё - работа окончена!

ну а кто же по вашему должен писать техдокументацию на этот код?

Кто же кроме тех кто писал этот код знает что там написано?
Конечно же кто пишет код - тот и должен писать техдокументацию.

Не можем же мы менеджера по клинингу все время напрягать писать нам техдокументацию - так он обидится и некому будет убирать офис!? :)

Подождите путать мягкое с теплым.

По заказу бизнеса появился в апи новый метод - выдать баллы системы лояльности у этого клиента.

Бек нарисовал запрос и ответ, что дальше?

Как фронт узнает что появился такой запрос?

Просто пытаюсь понять как вы предполагаете (если есть рабочие примеры - в студию, плиз) это должно работать.

Бэк описал это в typescript и опубликовал, к примеру:

newMethod: (params: Params) => ResultType;

разработчик приложения обновляет пакет с описанием интерфейса и в приложении просто делает вызов этого метода (IDE подскажет правильный синтаксис метода и типы параметров)

const result = await api.newMethod({... params ...});

всё! Proxy все сделает за вас по указанным в нем правилам!

В статье в коде все уже написано - попробуйте поиграться с кодом и у вас все станет на свои места

вот к примеру описание гипотетического API для taskList примера

export interface Api {

    getTasks: () => Promise<JsonRpcResponse<TasksData>>;

    createTask: (task: NewTask) => Promise<JsonRpcResponse<Task>>;

    updateTask: (task: Task) => Promise<JsonRpcResponse<Task>>;

    deleteTask: (task: Task) => Promise<JsonRpcResponse<Task>>;

    clearTasks: () => Promise<JsonRpcResponse<Task[]>>;

}

в коде приложения пишем методы вызова

const api = createApi('https://your-domain/api');

const result = await api.createTask({ title: 'newTask' });
...
const result = await api.deleteTask({ id: 1, title: 'newTask' });

Не сочтите душнилой.

Бэк описал это в typescript и опубликовал,

- с какого перепугу бек должен этим заниматься? Бек выдал новый контаокт - тоже самое +еще новый метод.

Это не обесценивание вашей работы, я просто смотрю можно ли это применить в реале

а как фронт по вашему должен узнать о сделанных изменениях?

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

- второй вариант - написать Proxy объект и обновлять только типы (код в слое API не меняется вообще никогда - нет необходимости генерировать или менять его) После обновления типов - менять вызовы API в приложении

Вы же сами себе противоречите.

Разумеется апи будет меняться, это факт.

Собственно, если изменяетя апи, то и изменяется прокси..

Вы не сможете вызвать метод если он не прописан в прокси.

Или я чтото не так понял, и ваш объек автоматически предоставляет все методы, которые есть в апи?

в этом то и большой кайф, что не нужно ничего менять - прокси может делать вызовы не существующих методов!!!
Почитайте про возможности Proxy в js

Вы можете это продемонстрировать на примере ?

"прокси может делать вызовы не существующих методов!!!"

код, что написан в статье демонстрирует это - возьмите его и попробуйте выполнить в консоли
Попробуйте делать вызовы не существующих методов ...

Позже я сделаю демо-проект и ссылку прикреплю к статье

Да, было бы очень интересно.

Если такое можно применить в работе:)

Это некая аналогия с использованием рефлексии в .net.

Объявляется интерфейс IMyCoolApi, в нём - методы, типа, getBlog(id). Реализацию этого метода вы не пишете. Но вы пишете прокси-класс, который вслепую реализует любой метод интерфейса IMyCoolApi.

Прокси-класс смотрит название вызванного метода. Если название начинается на get, то готовится GET вызов. То, что идёт после get добавляется в URL запроса. Например, getBlog преобразуется в GET .../blog. Аргументы тоже анализируются и добавляются в запрос.

И не важно что в будущем добавится в интерфейс IMyCoolApi - прокси-класс уже по факту вызова по названию метода и аргументам сформирует HTTP запрос.

Нифига. Если бы было такое - я бы тут не возражал.

Итак, кейс: бек добавил по требованиям бизнеса новый метод.

Что происходит дальше?

завтра выложу код - попробуете руками и станет все на свои места

А где хранится код показанный после вот этой строчки "Давайте я вам покажу, как это работает на примере:"?

этот код хранится на клиенте - это API слой приложения

или вы имели в виду что-то другое?

Я имею в виду, что у вас есть код, который:
- хранится в каком-то специальном файле
- содержит функции, используемые многократно

Как именно Вы называете место, где хранится этот код, если не "библиотека"?

И почему этот файл не называется "библиотека" ?

в статье я написал, что компания может оформить код в отдельную библиотеку и использовать на разных проектах, но написать отдельную публичную библиотеку для всех сложно - требование к API в каждой компании своё - у кого-то REST, у кого-то RPC и т.д.

Для подобных целей рекомендую использовать паттерны проектирования Proxy и Адаптер.

Посыл статьи понял, довольно интересное решение проблемы(возьму себе на заметку). Но после прочтения некоторых комментариев, возникло ощущение что в web чуть-ли не с улицы набирают...

ради одного такого комментария стоит писать статьи - спасибо, что хоть кому-то пригодилась идея!

начал было разочаровываться в сообществе - минусуют без аргументов (скорее всего не поняли идеи)

Как-то идея писать что-то, что умеет вызывать несуществующие методы, не вызывает у меня особого доверия

согласен - это может пугать, но у нас есть хороший полицейский в виде typescript - он не даст разгуляться фулигану :)

Как я понял, генератор TypeScript интерфейса просто переложили с фронта на бэк. Делаться он будет на основе того же описания со swagger. Просто это будет делать разработчик с бэка, когда ему захочется что-то поменять. Фротендеру после этого придется дорабатывать ядро. Суммарное количество затрачиваемого времени на работу не изменилось, просто оно уменьшилось на фронте и увеличилось на бэке. Фронт рапортует об ускорении процесса. По ощущениям, ускорение будет незначительным, так как что там, что там работу делает генератор. А если бэк будет вручную править, то времени придется потратить даже больше. В чем профит для бизнеса? Обработка несуществующих методов может быть решена один раз на стороне бэка выдачей 404 и все.

Возможно я что-то не совсем понял и автор сможет пояснить детали.

бэки при сборке кода автоматом от tsc получают описание интерфейса и оформляют его в новую версию пакета
Фронты импортируют только этот интерфейс и ничего не исправляют кроме изменившихся вызовов в приложении
Прокси объект не меняется никогда

Удалили метод из апи. Что произойдет? Тайп скрипт начнет ругаться что метода нет? Чем прокси отличаеться от фасада? Или другой вариант, изменился протокол - был http стал веб сокет? По идее в это то как раз случае и надо прокси просто поменять? Смущают слова чтл прокси никогда не меняется

вы часто на проектах наблюдаете перезод с одного протокола на другой?
ну случится этот переход один ну максимум 2 раза за все время существования компании - тяжело переписать меньше килобайта кода? )

Зависит от задач. Не попадайте в ошибку серебренной пули одного проекта. В итоге то что - прокси все таки меняем? Я пытаюсь понять Вашу идею, а не критикую.

теоретически только тогда когда меняем систему требований к АПИ

Вместо поддержки слоя API вы предлагаете поддерживать слой прокси. Апи может так поменяться, что никакой прокси слой не спасет. Ну и потом, не весь бэк на ноде пишется и не везде на бэке используется тайпскрипт. Все это как мёртвому припарка.

ну почему никто не читает статью! )))
там написано что прокси пишется только один раз вначале и не меняется больше никогда!
меняются только типы

Статью я прочитал, но видимо не понял всех плюсов решения. Давайте разберём реальный кейс. Допустим, у нас была логика: запрашиваем URL url1/common1, возвращается структура

{
  "key1":"val1",
  "key2":"val2"
}

API поменялось, теперь надо запрашивать 2 URL'а: url/entity2 и url/entity3, возвращается соответственно

{
  "key1": {
    "key3": "val1"
  }
}

и

{
  "key4": "val2"
}

Какой должен быть интерфейс старого API и как мне нужно его поменять на стороне клиента, чтобы адаптировать под новый API?

так как у вас прекратил существование common1  и апоявились 2 новых метода: entity2 и entity3 вам соответственно в интерфейсе API нужно удалить описание старого метода и добавить описание 2х новых - все!

Когда типы будут обновлены на клиенте - появится ошибка в приложении на удаленном методе, что такого метода не существует - удаляете в приложении старый вызов и делаете 2 новых

АПИ слой на клиенте не трогаете и не меняете

Вы забыли, что надо ещё поменять логику запроса этих данных. Ранее одного запроса было достаточно, теперь надо делать два. Причем может случиться так, что нагрузка запроса тоже поменялась. Тут простым изменением интерфейса тайпскрипта не обойтись. Придется менять логику получения данных и их обработку. Если все это инкапсулировать в отдельный класс/объект/модуль, то это модуль в любом случае придется постоянно переписывать при таких изменениях.

ну если это относится к логике порядка вызовов в приложении - ее нужно реализовывать в любом варианте реализации слоя АПИ
а если вы хотите сказать что это было в слое АПИ - тогда ваш слой АПИ выполняет не свойственные ему функции - он знает о чужой логике и правильнее было бы вынести эту логику либо на сервер либо в приложение где слой юзкейсов знает о таких вещах

слой АПИ в приложении должен выполнять только транспортную роль и должен быть простым передастом )

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

если убрать не свойственную АПИ логику из АПИ слоя - все вопросы решаются легко, а так получается что бизнес-логика последовательности вызова протекает в сервисный слой который ничего не должен знать о работе приложения вообще

Я вам про Ивана, вы мне про ... Мы не обсуждаем, что и куда утекло. У вас в статье написано

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

Как при измерении апи в моем примере ваш подход избавляет от сложностей, о которых вы сами написали, если все равно приходится менять логику запроса?

да я же вам написал - никаких сложностей - как и при генеративном подходе как и при ручном так и при использовании Proxy вам в любом случае в коде приложения нужно заменить удаленный вызов на 2 новых.

В 2х первых вариантах - вам еще нужно эти изменения привнести в АПИ слой - в моем варианте этого не нужно делать

надеюсь мы пришли к общему пониманию )
прошу прощения, но по вышезаданным вопросам я не уловил акцент

Это вы меня извините) На моей практике изменение именно апи слоя не вызывало каких-то серьезных затрат. Значительное время обычно занимала как раз изменение логики работы с апи и подготовки данных запроса и ответа. Поэтому название статьи интерпретировал не так, как вы задумыаали.

название придумал ИИ )
я просил чтобы оно было привлекательным для чтения - вот он и придумал такую провокацию )

Вы предлагаете вместо такого кода:

class API extends BaseClient {
    async getTodos(): Todo[] {
       return await this.get('/todos') as Todo[];
    }
    async postTodo(title: string): Todo {
       return await this.post('/todos', { title }) as Todo;
    }
    async deleteTodo(id: number): Todo {
       return await this.delete('/todos', { id }) as Todo;
    }
+   async postNewMethod(param: string): Result {
+      return await this.post('/new-method', { param }) as Result;
+   }
}

писать такой:

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

В чем принципиальная разница?

Ну тут явный профит в том что вы описываете только интерфейс без каких либо реализаций

Реализация один раз написана, покрылась тестами и больше вы ее не трогаете, а расширяете свой cdk только за счет описания новых интерфейсов

Тесты на первый пример кода покажут, что он правильно возвращает объекты типа Todo или ошибку для разных ответов сервера. Тесты на реализацию прокси-объекта без конкретного интерфейса этого не покажут. Это то же самое, что не использовать тесты в первом примере.

Ну тестируется ведь только какая-то ло логика, а тут вся логика состоит в том что нужно трансформировать названия вызываемых методов в url path, Это тестируется один раз и больше не меняется
А потом только описываются интерфейсы которые тестировать не надо, потому что нечего там тестировать
В целом идея того что бы описывать таким способом апи очень даже интересная, но это немного смахивает на магию если не будет написано документации на то что происходит

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

а тут вся логика состоит в том что нужно трансформировать названия вызываемых методов в url path, Это тестируется один раз и больше не меняется

Я именно про это и говорю. В обычном классе-клиенте нет логики трансформации, на него будут другие тесты, которые будут проверять какое-то другое поведение. А в случае интерфейс + прокси это поведение проверяться не будет, хотя должно, так как это замена обычного клиента. То есть это эквивалентно тому, как если бы мы просто не стали делать эти тесты на обычный клиент.

Какое другое поведение? Что вы хотите там еще протестировать? Слой апи отвечает за то что получает какие-то входящие данные идет с ними на сервер и возвращает ответ, все то же самое, просто тут описание апи идет за счет интерфейсов без написания дополнительной логики
Если у вас есть какие-то трансформации данных от сервера то это перейдет на другой слой и будет тестироваться там

В вашем примере выше все то что написано в классе после компиляции пойдет в рантайм, описание интерфейсами никуда не пойдет, в рантайм пойдет одна функция с прокси которая будет трансформировать все вызовы в тот код что вы написали руками

Это вы сказали, что реализация прокси протестирована, а реализация апи-клиента нет. Вам лучше знать, какое поведение вы хотели тестировать.

Я говорю о том, что в обоих случаях тесты должны быть одинаковые. Потому что тесты должны тестировать поведение для вызывающего кода, а поведение этих компонентов предполагается одинаковое. То есть если для апи-клиента есть тесты на метод newMethod, то и для прокси они должны быть на этот же метод. Если на прокси у нас меньше тестов, то их можно не делать и для апи-клиента.

Я согласен с аргументом про уменьшение размера кода, только в статье минимизация кода не описывается как основное преимущество.

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

Я вообще потерял нить сути этой ветки, вы спросили о разнице, она очевидна на первый же взгляд, как минимум то что интерфейс читается сильно проще чем класс, интерфейс расширить проще чем описать класс и каждый раз писать реализацию вызова к серверу

вы абсолютно правы - слой АПИ в приложении должен быть тупым передастом - его один раз протестировали что он выполняет свои функции по приёму/передаче данных в требуемых условиях и всё!
всё остальное его не касается - это не его зона отвественности

Если вы размажете бизнес логику и на этот слой - ну тогда миритесь со сложностью своего проекта и частыми багами где что-то изменили в одном месте в сотне других которые размазаны везде - забыли

как минимум то что интерфейс читается сильно проще чем класс

Я же написал, что можно сделать и с интерфейсом, это ничего не меняет, при чем тут именно классы?

interface API {
    ...
+   postNewMethod: async ({ param: string }) => Result;
}

let client: API = {
    ...
+   async postNewMethod(param) {
+      return await this.post('/new-method', { param });
+   }
}

С прокси-классами добавляем 1 строчку, с клиентом-объектом 1+3. Изменения исходного кода есть в обоих случаях, что противоречит словам автора. Просто в его проекте он переложил обновление интерфейса на другую команду. Если бы у него бэкендеры делали документацию в Swagger, ему бы пришлось делать генерацию TypeScript самому, что противоречит громким заявлениям в статье "больше никакой генерации".

она очевидна на первый же взгляд

В первом сообщении вы написали, что разница в тестах, теперь говорите про читаемость.
Вы сказали, что почему-то с прокси можно не писать тест на метод newMethod(), а с клиентом надо писать. Я вам объясняю, что надо писать в обоих случаях, потому что в обоих случаях есть объект с методом newMethod(), неважно как он организован. Или можно не писать в обоих случаях, код будет одинаково не покрыт тестами.

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

вопрос в коде - он не нужен! вы генерируете кучу методов выполняющих по сути одно и тоже но их много и они много весят!
в замен вам предлагается генерить лишь интерфейс на typescript а всю кучу однотипного кода свернуть до кода одного proxy-объекта который весит меньше килобайта приэтом будет сэкономлено тысячи часов процессорного времени на генерации кода а так же уменьшится нагрузка на сервера компании благодаря меньшему размеру бандлов

всю кучу однотипного кода свернуть до кода одного proxy-объекта

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

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

приэтом будет сэкономлено тысячи часов процессорного времени на генерации кода

Я вам уже объяснил, что это не так. "Процессорное время" тратится бэкенд-командой на генерацию TypeScript-интерфейса, который им самим не нужен. Вот точно так же вместо интерфейса они могли бы генерировать клиент. Вы не убираете генерацию, кто-то все равно должен это делать.

Если мы будем использовать прокси-объекты в нашей команде, TypeScript-интерфейс будут генерировать фронтендеры, поэтому утверждение, что ваш подход позволяет избежать генерации, это ложь.

согласен - найти прям огненную фразу которая бы расставила все точки над Ё достаточно сложно сразу - порой приходится пройти кучу споров и объяснений чтобы найти точную формулировку!

Спасибо что помогаете найти смысл - добавлю ее в текст

(но данное решение и правда позволяет избегать постоянного переписывания кода слоя АПИ в приложении тоже ? )

Вы приписываете мне то что я не писал, я нигде не писал что надо тестировать класс из первого коментария или что-то подобное, я конкретно писал про тестирование прокси и все.
И что в первом коменте что дальше я писал про разницу в простоте между просто интерфейсом и классом(функцией) с каким-то дополнительнім кодом
Тестировать все это на возвращение объектов определенного типа собирался товарищ что отвечал мне на коменты
В моих коментах нет ничего кроме тестирования прокси что он правильно работает и все, никакие тесты не нужны больше

Что касается вашего примера выше, так вот:

Для реализации с прокси вам этот кусок кода вообще не нужен, будь это класс, функция или просто объект, неважно как вы хотите это реализовать, это попросту не нужно

let client: API = { ... + async postNewMethod(param) { + return await this.post('/new-method', { param }); + } }

я конкретно писал про тестирование прокси и все

Я спросил "В чем принципиальная разница [между прокси и обычным клиентом]?". Вы ответили "Реализация [прокси] один раз написана, покрылась тестами". Значит вы говорите, что на клиент надо писать какие-то дополнительные тесты, чтобы эти подходы были эквивалентны.

В моих коментах нет ничего кроме тестирования прокси что он правильно работает и все, никакие тесты не нужны больше

Еще раз повторяю, я это прекрасно понял, и говорю о том, что это неверное утверждение. В плане тестирования никаких различий между этими подходами нет. "Тесты на реализацию прокси-объекта" эквивалентны "тесты на this.get/this.post/this.put/this.delete" в обычном клиенте. Если во втором случае мы считаем, что их недостаточно и надо делать тесты на "this.newMethod", то и в первом их недостаточно. И наоборот, если достаточно, то достаточно в обоих.

Для реализации с прокси вам этот кусок кода вообще не нужен

Я знаю, я сам написал в первом комментарии 2 примера, которые различаются именно этим. Мне непонятно, зачем вы мне это повторяете.

Вот вам более явные примеры как это предлагается делать в случае с прокси в сравнении с каким-то класическим способом описания апи

// Client with Proxy implementation
function api<T extends {}>(): T {
  // Proxy implementation
  return null;
}

export interface UserApi {
  getUser: (id: number) => Promise<any>;
  getUsers: () => Promise<any>;
  addUser: () => Promise<any>;
  updateUser: (id: number, payload: any) => Promise<any>;
}

export interface StoreApi {
  getStore: (id: number) => Promise<any>;
  getStores: () => Promise<any>;
  addStore: () => Promise<any>;
  updateStore: (id: number, payload: any) => Promise<any>;
}

const userClient = api<UserApi>();
const storeClient = api<StoreApi>();

// Common Clients
export class UserClient {
  getUser(id: number): Promise<any> {
    return fetch(`/user${id}`)
  }
  getUsers(): Promise<any> {
    return fetch(`/users`)
  }
  addUser(): Promise<any> {
    return fetch(`/users`)
  }
  updateUser(id: number, payload: any): Promise<any> {
    return fetch(`/users${id}`, payload);
  }
}

export class StoreClient {
  getStore(id: number): Promise<any> {
    return fetch(`/store${id}`)
  }
  getStores(): Promise<any> {
    return fetch(`/stores`)
  }
  addStore(): Promise<any> {
    return fetch(`/stores`)
  }
  updateStore(id: number, payload: any): Promise<any> {
    return fetch(`/stores${id}`, payload);
  }
}

const userClient = new UserClient();
const storeClient = new StoreClient();

Я это прекрасно знаю, и написал такой пример в первом комментарии. Я не знаю, зачем вы мне это пишете.

ну как бы мягко сказать что мы классы не используем совсем

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

вы не правы - посмотрите прикрепленный демо проект с описанием действий

Посмотрел, ничего противоречащего моим словам не нашел.
Ваш export interface API будет точно так же увеличиваться при добавлении новых эндпойнтов на сервере. Кто-то должен будет добавлять туда новые строки.

вы предлагаете распухающий со временем код прослойки который нужно постоянно генерировать или править руками я же предлагаю 0 изменения кода прослойки и ничего не нужно генерировать и ничего не нужно в ней править

да, и тестировать в ней тоже нечего - тестируется сервер

я же предлагаю 0 изменения кода прослойки

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

и ничего не нужно генерировать

А интерфейс с описанием методов откуда берется, по волшебству? Нет, по вашим словам его генерирует бэкенд-команда.

нет не будет ни на байт распухать прокси-слой - там нет методов АПИ - они появляются только в рантайме в момент их вызова в приложении (псомотрите демо проект)

да, тот кто пишет код - тот его и описывает в типах - извините фронтэндщики не телепаты! )

- Ваш интерфейс точно так же будет распухать
- нет не будет ни на байт распухать прокси-слой

При чем тут прокси-слой, если я сказал про добавление новых строк в интерфейс?

да, тот кто пишет код - тот его и описывает в типах

Тогда почему вы говорите, что "ничего не нужно генерировать", если генерировать нужно?

Я вот написал на бэкенде код, описал его в Swagger, отдал фронтендерам, что дальше?
TypeScript я генерировать не буду, у меня в PHP нет компонентов для этого, а для Сваггера есть.

поcмотрите демо проект

Я вам выше написал, что посмотрел и не нашел противоречий с моими утверждениями.

я понял вас - генерировать интерфейс кому-то придется а кому-то и нет - все зависит от того на чем пишут и как
мы про разное "пухнуть" говорили - я имел в виду что код апи слоя никак не будет изменятся а типы в конечном коде не участвуют и на размер бандла не влияют - могут и попухнуть немного )

"Апи слой" это интерфейс + прокси в одном случае и объект апи-клиента с реализацией и типами в другом. Отдельно прокси это не апи слой, иначе можно и обычный fetch назвать апи-слоем, потому что он может отправлять любые запросы.

Я так понимаю вы придумали CDK. Пусть внутри оно работает на основне прокси, но для клиента который это использует вообще нет никакой разницы как оно реализовано внутри. Интересная идея трансформации названий методов в url path, но я вижу упрощение только в этом.

В остальном все равно придется писать еще один слой для работы с этим cdk и получается что теперь есть прокси слой который пишет бекенд + слой на клиенте который будет работать с этим cdk вместо одног api слоя на клиенте.

Все же я больше профита вижу в том что бы бекенд хорошо описывал openapi документацию а клиент на основе этого генерировал себе все что ему нужно

у нас профит в том, что данный подход хорошо ложится на используемый у нас протокол JSON RPC и мы ничего не пишем и не генерируем - в процессе сборки бэк получает сгенерированный tsc интерфейс и отправляет его в пакет.
Фронты импортируют новую версию и патчат только код приложения

демка: https://github.com/budarin/proxy-api-demo

Мне нравится. Буду рад увидеть реализацию в коде.

Весь вопрос с том: будет ли экономия эффектной или нет.

Вот, например, у меня проект торгового бота для биржи по REST API.

Версии АПИ периодически меняются, может измениться:

Синтаксис команды

Параметры запроса

Параметры ответа

Порядок параметров и их имена

И, при этих изменениях, разумеется, приходится править синтаксис в своём модуле-адаптере.

Ваш подход может помочь в данном случае?

я уже отвечал на подобный вопрос - если АПИ не придерживается никакой системы - да такой подход не подойдет!
Данный подход предполагает что АПИ имеет четкие правила и ограничения и они системны

Это не АПИ, а соглашение о протоколе обмена. Теперь похоже понятно - Вы вводите в заблуждение и, похоже, даже себя, называя это соглашение АПИ. АПИ это интерфейс а не способ передачи данных или его реализация.

Drf-spectafular + rtk query cod egen с ts.

При изменении и перегенерации tsc укажет на все ошибки.

Иметь еще интерфейс-прокладку на полсотни ендпоинтов не выглядит весело. Потому что надо будет все эти мутации и квери надо будет дублировать, зачем я тогда их генерировал? Что бы всеравно ручками описывать?

Чем это лучше GraphQL?

Ок, имеем один сервис, который выступает бэком для фронта, а может и не только фронта. А еще десятки сервисов, которые дергают друг друга, в особо сложных случаях еще и написанных на разных языках. Сваггер полностью закрывает вопрос описания их API и генерации клиентов. Вы же предлагаете бэкендерам поддерживать непонятно как генерируемый абсолютно чуждый для них .tsc-файл. План надежный, как сами знаете что)

нет не предлагаю - я предлагаю вам как и раньше при помощи инструментов того же swagger генерировать только описание интерфейса на typescript!
Для бэкэндеров ничего в процессе не меняется, а вот во фронтенде - приложение получает большой плюс в виде отсутствия пустого кода

Не совсем понял. Сваггер генерит OpenAPI-контракт без привязки к языку. По нему уже, кому необходимо, генерит клиент, в том числе для ts.

Понятно! Вы как и все остальные минусующие бэкэндеры прочитали заголовок и максимум 1й абзац и статью не прочли и кинулись писать гневный коммент ))

Вся статья и все комменты о том что не нужно будет при моем подходе НИКОГДА БОЛЬШЕ В ЖИЗНИ писать/генерировать код для клиента!!! )

Все мимо: и не минусовал, и статью прочел целиком, правда со второго раза, когда понял по комментам, что происходит нечто загадочное)

Ваша цитата:

разработчики бэкэнда должны будут изменить описание интерфейса и опубликовать его новую версию

Это что по-вашему, как не генерация клиента для API, пускай без кода методов, но описания dto-шек-то как обновятся, да и набора методов? И что прикажете, для каждого вызывающего сервиса этим заниматься? Это вовсе не ответственность разработчиков API.

Некоторые из них прибегают к автогенерации кода на основе схем Swagger, другие переписывают код вручную.

Изменить = сгенерировать новую версию при помощи инструментов (их есть для генерации интерфейсов на typescript) или для тек кто пишет руками - изменить руками.

Разработчик АПИ будет делать все то же что и раньше только инструменты его не будут генерировать код а будут генерировать только описание интерфейсов

скажу даже больше - у нас микросервис АПИ (тот который не имеет своей логики и является передастом между клиентом и БД) не изменяется вообще никогда - меняются/генерируются лишь JSON схемы и сервису лишь подсовывается новая схема при его рестарте
Мы вообще прагматичные лентяи )))

в начале всегда возникает не приятие - новое не понятное, но если понять - то потом не оторвешь за уши :)
старый подход предлагает генерировать тонны однотипного кода - мой метод предлагает его вообще не генерировать и облегчить жизнь фронтендерам и юзерам, но все в начале протестуют - не хотят ни добра фронтэндерам ни радости юзерам ну потому что привыкли уже по-старинке)) а зачем что-то менять? ))

Спасибо за статью. Очееь интересно было почитать про применение Proxy (не работал с ним).

Судя по комментариям, многие смешивают понятия.

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

Вы всё верно поняли из статьи!

а как происходит обработка ошибок при таком подходе ? если запрос упал?

Мы у себя используем JsonRpc и не имеем проблем со смешиванием кодов состояния сети с кодами состояний данных из АПИ:
- ошибки транспортного уровня обрабатываем и отдаем как ошибки транспортного уровня
- ошибки АПИ напрямую проходят к потребителю ровно так же как и успешные запросы

Ну а в общем - данный слой должен любую ошибку прямо передать потребителю - в нём не должно быть никакой логики. Ставите try/catch на fetch() и json() и в случае возникновения ошибки просто возвращаете объект с ошибкой

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории