Подробнее о JSON RPC
Не хочу разводить очередной холивар на эту тему. Поэтому, если вкратце, то GraphQL — это сложно, RPC — быстро, REST — некий медиум, но не хватает batch-запросов. И если у вас небольшое приложение или микросервис, то rpc, он же “вызов удаленной процедуры”, может оказаться гораздо лучше и экономичнее для вашей архитектуры, особенно если она основана на микросервисном подходе.
На моей практике это решение оказалось достаточно удобным и простым, но при этом мощным и расширяемым. И меня сильно удивило, что в мире node.js оно применяется крайне редко.
Суть подхода достаточно примитивна
Запрос включает в себя 4 поля:
- jsonrpc — всегда будет “2.0”, указывает версию протокола.
- method — название метода (функции), который нужно вызвать.
- params — опциональное поле, нагрузка к вызову (аргументы функции).
- id — опциональное поле, уникальный идентификатор вызова. Если вы хотите получить значение от вызванной функции, то вы должны сгенерировать id на стороне клиента и при ответе вы сможете понять, на какой именно вызов пришел ответ, сопоставив id ответа.
Если вы не отправили id, то это означает, что ответ вас не интересует и от сервера вы ничего не получите. Такой вызов называется нотификацией.
Ответ может иметь следующие поля:
- jsonrpc — всегда будет “2.0”, указывает версию протокола.
- result — тело ответа (возвращаемое значение функции).
- id — уникальный идентификатор ответа. Он нужен для того, чтобы клиент мог сопоставить, на какой запрос он получил ответ.
- error — в случае ошибки вместо result, вы получите поле error, содержащее в себе code (код ответа: по протоколу их может быть шесть) и message (человекопонятное описание ошибки).
Вишенка на торте — это Batch-запрос!
Это означает, что мы можем слепить несколько отдельных ajax запросов в один и отдать его серверу в виде массива запросов. А при получении по заданным id мы поймем, на какой запрос получили ответ.
Небольшая тонкость!
Учтите, что по протоколу сервер не гарантирует последовательность элементов в ответе, например:
Будет считаться вполне нормальным ответом, т.к. время выполнения функций разное, и сервер заполняет массив ответами по мере их асинхронного выполнения.
Поэтому клиент сам должен промапить ответ и сопоставить id каждого элемента в ответе с запросом.
Давайте уже писать код!
Поскольку в последнее время я пишу на node.js и готового решения, которое бы полностью меня устроило, я не нашел, то я решил запилить простой JSON-RPC роутер для express.js без каких либо зависимостей.
В документации к пакету базовые примеры уже рассмотрены. Давайте придумаем вариант посложнее.
Например, создадим сервер с тремя методами, которые будут получать данные по http от другого ресурса. Каждый метод будет проверять, что пользователь ввел валидные параметры, затем выполнять тело метода и логировать результат выполнения в файл.
Сначала мы создадим папку с файлами, инициализируем проект и подключим к нему зависимости.cd /path/to/your/proj
mkdir medium-json-rpc
cd medium-json-rpc
yarn init -y
touch index.js
yarn add express axios body-parser express-json-rpc-router
У нас имеется три основных объекта:
methods — наши JSON-RPC методы, функции которые реализуют основную логику. Это могут быть ваши контроллеры.
beforeMethods (опциональные) — хук, который будет вызван перед выполнением метода. Здесь вы можете валидировать переданные параметры и выкидывать ошибки, которые попадут в вышестоящий обработчик. Название хука должно совпадать с названием метода, к которому этот хук относится, иначе будет вызвано исключение с сообщением, содержащим информацию о том, какой хук был назван неправильно.
afterMethods (опционально) — хук, который по своей логике похож на предыдущий. Разница только в том, что он будет вызван после выполнения метода и в качестве второго параметра получит результат выполнения. Может быть удобен для логгирования запросов.
Также предоставлен callback (onError) — функция, в которую будет передана ошибка, если она случилась. Тут можно удобно подключить логгер ошибок вроде sentry.io.
Обратите внимание, что bodyParser вызван перед роутером. Можете использовать другой вариант парсинга тела запроса, главное, чтобы в req.body мы получили параметры запроса в формате JSONRPC.
Библиотека в стадии разработки, так что если у вас есть пожелания или идеи, смело пишите в комментариях или открывайте github issues.
Всем хорошего настроения!
Правильный JSON API или JSON RPC
Приходим к выводу, что JSON-RPC это: удаленный обмен данными.
Наверняка этот обмен данными будет происходить с неким интерфейсом, т.е. с API.
И в чем проблема?! Спросите Вы. А в том, что некоторые программисты разрабатывая JSON API, т.е интерфейс, забывают про JSON-RPC.И начинается очередное изобретение велосипеда. Frontend программист говорит: «я передам тебе такой то json», а Backend программист отвечает: «а я тебе верну такой то json». И все бы ничего, но было бы хорошо вспомнить о том, что умные люди уже давно разработали стандарты, вернее протоколы обмена данными. И не какие то сверх сложные, а очень даже простые: JSON-RPC
Вероятно многие, если не сказать, что почти все, знают и даже пользуются этими протоколами. Написана куча серверов и т.д. Но вот лично меня не все устроило в существующих протоколах. Они показались мне не достаточно гибкими и не во всем логичными. Как вы уже догадались решил я изобрести свой велосипед json-rpc-1.5
Главные отличия от существующих протоколов это:
- Опциональный параметр «sign» — Signature (подпись) или Token
- В запросах вместо параметра «param» используется параметр «data», т.к. мы всегда отправляем данные, а не просто параметры.
- Во всех ответах всегда возвращается параметр «result» и в нем находится описание результата запроса «success» или «error».
- Все данные в ответах приходят в параметре «data»
- Можно использовать алиасы для названия параметров запроса и ответа
PS:
Получив кучу отрицательных комментариев и минусов, решил еще раз проверить, может я действительно, что то не так делаю? Естественно, всё что я здесь пишу, это мое личное мнение и я никому ничего не навязываю. Приведу пару примеров:
1. Пример запроса JSON API Yandex директ:
У них же можно почитать и про токены: Авторизационные токены
2. Пример из Сбербанк API Оплата из мобильного приложения с использованием Apple Pay
JSON запрос приводить не буду, он большой, по ссылке можно посмотреть.
Важно, что JSON запрос содержит «paymentToken». Вот ссылка на требования к формированию токена от Apple
Важно понимать, что токены и подписи в API используются часто, естественно на ряду с другими методами защиты. И те кто работает с разного рода API, прекрасно это знают.
Популяризация JSON-RPC (часть 1)
Для передачи данных по сети есть хорошо зарекомендовавшие себя стандарты. Среди основных: SOAP, gRPC, AMQP, REST, GraphQL.

При создании вебсайтов малой, средней и большой сложности с потоками данных к бэкенду и обратно в JSON формате обычно используются последние два с их вариантами. Верней, только варианты, потому что REST и GraphQL — ресурсо-ориентированные стандарты. Это как бы просто перенос элементарной работы с базой данных на клиента (хотя под «ресурсом» может пониматься и абстракция). Обычно таких запросов не больше трети от всего бэкенд API.
Попытка сделать весь API максимально RESTful страшно раздувает код и грузит сеть. Потому что остальные две трети запросов — в форме команд на бэкенд проделать какие-то действия, слабо отображающиеся на CRUD над некими ресурсами. И вариантов послать такие запросы достаточно много. Даже, чересчур.
GET, POST, PUT, PATCH, HTTP заголовки, куки, body, данные форм, GET query параметры, json, content-type, HTTP коды. Когда в команде несколько программеров, и у каждого свой взгляд на мироустройство, довольно быстро это превращается в винегрет. Даже один фулстэк разработчик часто оказывается в тупике перед всем этим месивом параметров, глаголов и существительных RESTlike API в непонимании как дальше с этим жить.
Всё это привело к созданию простой и понятной, но с моей точки зрения сильно недооцененной спецификации JSON-RPC (https://www.jsonrpc.org/specification), которая отделяет бизнес-логику клиент-серверного запроса от самого сетевого протокола (HTTP) с его богатым, но не всегда нужным внутренним миром.
В JSON-RPC все запросы стандартизовано идут через HTTP POST в форме JSON объекта (в принципе, JSON-RPC 2.0 — это транспорто-независимый протокол, но мы рассматриваем наиболее частое его употребление). Обмен данными строгий и понятный. В запросе есть method и params . Method играет роль эндпойнта/команды, params — параметров. Ответы сервера приходят примерно в таком же виде.
RPC означает Remote procedure call, то есть, на бэкенд посылается команда о выполнении некоего кода. Команда по смыслу и предназначению может быть любой. В этом отличие RPC от REST, ограниченного четырьмя действиями CRUD на неких ресурсах.
JSON в названии означает, что обмен информацией между клиентом и сервером (микросервисами) идет через данные JSON формате.
Подробней о JSON-RPC можно прочитать, например, тут — https://habr.com/ru/post/441854/ или во многих других местах. Ничего сложного. В данной статье я хотел бы сконцентрироваться именно на его плюсах, а не описании.
Также, для задумывающихся о грамотном дизайне networked API рекомендую статьи Google на эту тему — https://cloud.google.com/apis/design. Архитектура для API не менее важна, как и для самой программной системы. Есть даже такое направление — API Driven Architecture, хотя по-моему опыту сперва обычно всё-таки делается какая-то альфа версия приложения, а потом уже рефакторится и унифицируется его API.
Плюсы
JSON-RPC отделяет бизнес логику от сетевого протокола. На этом, на самом деле, можно было бы запустить фейерверк и закончить. Да, он облегчает коммуникацию между разработчиками фронта и бэка, даёт лучшую структуру и понимание данных, простоту в разработке, протоколо-независимость, но, главное, с моей точки зрения, именно вышеуказанное отделение.
Что значит это для фронтэндера? Я обычно делаю модуль доступа к API и работаю через него:
import api from '@/api'; api.products.list(); api.users.update(userId, );
Внутри list() и update() запросы через fetch() или axios() к нужным эндпойнтам backend API. Переход на стандарт JSON-RPC несложен, может быть проведен постепенно и особо профита в кодинге не приносит, кроме отсутствия необходимости задумываться, что использовать — POST, PUT или PATCH, и как передавать параметры, как обрабатывать приходящий результат, ошибки и т.п.
Совсем другое дело бэкенд. Я большой фанат CodeIgniter 4 (далее CI), считаю его лучшим PHP фреймворком для малых и средних по размеру API, и буду говорить на его примере, но Laravel, Sping Boot, Django работают примерно по тому же принципу.
На бэке каждый запрос обрабатывается контроллером (которые сейчас по сути являются уже рудиментом MVC архитектурного шаблона времён генерации контента на сервере), в который передаются (условно) HttpRequest и HttpMessage. Один контроллер может обрабатывать несколько эндпойнтов, это прописывается в роутинге фреймворка. В контроллерах есть доступ к деталям транспортного (HTTP) протокола. Зачастую в контроллерах лежит вся бизнес логика, включая работу с БД и другими внутренними сервисами.

Что происходит, когда вы решаете сменить фреймворк, потому что нашли лучше? Вы переписываете контроллеры. Что происходит, когда ваш фреймворк радикально обновился, и вам нравятся эти новшества, вы хотите их использовать, но там много breaking change изменений? Вы переписываете контроллеры. А это бОльшая часть кода. И перейти на новую версию постепенно довольно сложно, только разом. Всё потому что ваша логика встроена в фреймворк.
А что с JSON-RPC? Там есть только один контроллер, который обрабатывает все запросы и перенаправляет их на нужные модули своим внутренним роутингом. Вот весь HTTP роутинг в CI:
$routes->post('rpc', 'JsonRpcController::index');
Вот файл внутреннего роутинга, используемый JsonRpcController-ом:
[ "transactions:list" => "Users\Transactions::list", "withdrawal:create" => "Users\Transactions::createWithdrawal", ], "utils" => [ "resources:list" => "Utils\Resources::list", "resources:update" => "Utils\Resources::update", "resources:getByKey" => "Utils\Resources::getByKey", "resources:updateByKey" => "Utils\Resources::updateByKey", "resourceCache:clear" => "Utils\Resources::clearCache", ], ]; public static function route($method) < $path = explode('.' , $method); $route = self::$routes; foreach ($path as $step) < $route = $route[$step]; >return $route; > >
Иерархическая модель в $routes задана для удобства. Функция route() берет метод запроса ( method — «плоский» вариант роута, например, users.transactions:list) и выдает соответствующий ему класс и функцию на нем.
Выбор нотации названия метода полностью за разработчиком, но мне нравятся рекомендации Google из вышеприведенной ссылки.
Utils\Resources и Users\Transactions — просто PHP классы, отвечающие за бизнес логику. Им приходят данные в виде объекта, и они отдают результат в виде объекта. Никакой связи с HTTP протоколом. Если потребуется сменить фреймворк, то нужно будет переписать только один файл — JsonRpcController.php
request->getJSON(); > catch (\Throwable $th) < return $this->response->setJSON($this->errorResponse(-32700)); > $response = null; try < // batch payload if (is_array($payloadData)) < if (count($payloadData) == 0) < return $this->response->setJSON($this->errorResponse(-32600)); > $response = []; foreach ($payloadData as $payload) < $singleResponse = $this->processRequest($payload); if ($singleResponse != null) < $response[] = $singleResponse; >> if (count($response) > 0) < return $this->response->setJSON($response); > // single request > else if (is_object($payloadData)) < $response = $this->processRequest($payloadData); return $this->response->setJSON($response); > else < return $this->response->setJSON($this->errorResponse(-32700)); > > catch (\Throwable $th) < return $this->response->setJSON($this->errorResponse(-32603, null, [ "msg" => $th->getMessage(), "trace" => $th->getTrace() ])); > > /** * Process single JSON-RPC request. * * @param object $paylod Request object * @return object Response object */ private function processRequest($payload) < if (!is_object($payload)) < return $this->errorResponse(-32700); > if (!property_exists($payload, "jsonrpc") && !property_exists($payload, "method")) < return $this->errorResponse(-32600); > $payload->context = new stdClass(); if ($this->request->currentUser ?? NULL) < $payload->context->user = $this->request->currentUser; > $route = JsonRpcRoutes::route($payload->method); if (!$route) < return $this->errorResponse(-32601, $payload->id); > list($className, $methodName) = explode("::", $route); $className = JsonRpcRoutes::$basePath . $className; $outcome = (new $className())->$methodName($payload); if (!property_exists($payload, "id") || !$outcome) < return null; >$data = [ "jsonrpc" => "2.0", "id" => $payload->id ]; return array_merge($data, $outcome); > /** * Used for generic failures. * * @param int $errorCode according to JSON-RPC specification * @return Object Response object for this error */ private function errorResponse($errorCode, $id = null, $data = null) < $response = [ "jsonrpc" =>"2.0", "error" => [ "code" => $errorCode, "message" => '' ], "id" => $id ]; if ($data) < $response["error"]["data"] = $data; >switch ($errorCode) < case '-32600': $response["error"]["message"] = "Invalid Request"; break; case '-32700': $response["error"]["message"] = "Parse error"; break; case '-32601': $response["error"]["message"] = "Method not found"; break; case '-32602': $response["error"]["message"] = "Invalid params"; break; case '-32603': $response["error"]["message"] = "Internal error"; break; default: $response["error"]["message"] = "Internal error"; break; >return $response; > >
Контроллер мог бы быть в три-четыре раза меньше, но мне захотелось реализовать требования спецификации JSON-RPC 2.0 по максимуму, в итоге половина кода — корректная обработка всевозможных ошибок. По сути же он просто берет роут из JsonRpcRoutes и вызывает соответствующий метод на нужном классе, передавая ему параметры.

Да, в бэкэнд фреймворках есть другие часто используемые сервисы — например, для упрощения доступа к данным в БД (Active records (не ORM) в CodeIgniter 4 — очень эффектное решение, генерирующее эффективный SQL, возможно, единственное, ради чего стоит использовать CI, а не чистый rpc.php как вход с вебсервера), и их надо будет адаптировать при возможном переезде, но при желании модуль бизнес-логики (Utils\Resources и Users\Transactions) можно написать на чистом PHP/Java/Python, либо самостоятельно использовать сторонние библиотеки и быть полностью независимым от фреймворка. А ведь фреймворки очень хотят привязать разработчиков к себе.
А насколько проще тестировать свои классы, а не контроллеры, насколько проще строить архитектуру приложения без ограничений фреймворка, его контекста, потока исполнения и магии.
Я уж не говорю о переиспользовании кода: вызвать из одного контроллера метод другого, чтобы применить его ответ для выдачи клиенту — задача не тривиальная. Потому что вход и выход в контроллерах — через протоколозависимые шлюзы фреймворка. Да, можно вытащить всю логику из контроллеров и использовать их как пустые обертки/прокси — но в чем тогда будет их смысл? Да и именно это и делает JSON-RPC, — в том числе.
Нынешние бэкенд фреймворки создавались когда всё еще генерировалось на сервере и клиенту посылался готовый html. Для всего этого нужны были серверные сессии, шаблонизаторы и прочие штуки серверных фреймворков. Для современных SPA-ориентированных бэкенд APIs всё это не нужно, это тяжелый груз, который часто как кандалы мешает работать с современными технологиями.
Вывод
JSON-RPC дарит разработчикам свободу.

К сожалению JSON-RPC всё еще относится к эзотерическому знанию, крутость которого понимаешь только после того как его попробуешь, а попробовать решаются только разработчики, достигшие определенного уровня усталости от жизни такой. Большинство продолжает самоделить каждый раз корявые рестоподобные велосипеды.
Я бы даже сказал, что JSON-RPC — единственный стандарт, который технически полностью можно реализовать при построении коммуникации с бэкендом в 90% современных web app и мобильных приложений (если не смотреть в сторону gRPC и экзотики). И который реально приносит пользу в DX. Попытка реализации других стандартов будет большим компромиссом (просто оставлю здесь слово «HATEOAS»).
Во второй части я остановлюсь на нескольких особенностях при работе с JSON-RPC, а именно: зачем нужны batch пакеты, аутентификация и авторизация, как видеть в DevTools / Network и логах вебсервера семантически понятную информацию о запросах, а не просто /rpc , и как стать господином бэкэнд фреймворка, а не его рабом.
Популяризация JSON-RPC (часть 1) 08.01.2023 12:31
Для передачи данных по сети есть хорошо зарекомендовавшие себя стандарты. Среди основных: SOAP, gRPC, AMQP, REST, GraphQL.

При создании вебсайтов малой, средней и большой сложности с потоками данных к бэкенду и обратно в JSON формате обычно используются последние два с их вариантами. Верней, только варианты, потому что REST и GraphQL — ресурсо-ориентированные протоколы. Это как бы просто перенос элементарной работы с базой данных на клиента (хотя под «ресурсом» может пониматься и абстракция). Обычно таких запросов не больше трети от всего бэкенд API.
Попытка сделать весь API максимально RESTful страшно раздувает код и грузит сеть. Потому что остальные две трети запросов — в форме команд на бэкенд проделать какие-то действия, слабо отображающиеся на CRUD над некими ресурсами. И вариантов послать такие запросы достаточно много. Даже, чересчур.
GET, POST, PUT, PATCH, HTTP заголовки, куки, body, данные форм, GET query параметры, json, content-type, HTTP коды… Когда в команде несколько программеров, и у каждого свой взгляд на мироустройство, довольно быстро это превращается в винегрет. Даже один фулстэк разработчик часто оказывается в тупике перед всем этим месивом параметров, глаголов и существительных RESTlike API в непонимании как дальше с этим жить.
Всё это привело к созданию простой и понятной, но с моей точки зрения сильно недооцененной спецификации JSON-RPC (https://www.jsonrpc.org/specification), которая отделяет бизнес-логику клиент-серверного запроса от самого сетевого протокола (HTTP) с его богатым, но не всегда нужным внутренним миром.
В JSON-RPC все запросы стандартизовано идут через HTTP POST в форме JSON объекта (в принципе, JSON-RPC 2.0 — это транспорто-независимый протокол, но мы рассматриваем наиболее частое его употребление). Обмен данными строгий и понятный. В запросе есть method и params . Method играет роль эндпойнта/команды, params — параметров. Ответы сервера приходят примерно в таком же виде.
RPC означает Remote procedure call, то есть, на бэкенд посылается команда о выполнении некоего кода. Команда по смыслу и предназначению может быть любой. В этом отличие RPC от REST, ограниченного четырьмя действиями CRUD на неких ресурсах.
JSON в названии означает, что обмен информацией между клиентом и сервером (микросервисами) идет через данные JSON формате.
Подробней о JSON-RPC можно прочитать, например, тут — https://habr.com/ru/post/441854/ или во многих других местах. Ничего сложного. В данной статье я хотел бы сконцентрироваться именно на его плюсах, а не описании.
Также, для задумывающихся о грамотном дизайне networked API рекомендую статьи Google на эту тему — https://cloud.google.com/apis/design. Архитектура для API не менее важна, как и для самой программной системы. Есть даже такое направление — API Driven Architecture, хотя по-моему опыту сперва обычно всё-таки делается какая-то альфа версия приложения, а потом уже рефакторится и унифицируется его API.
Плюсы
JSON-RPC отделяет бизнес логику от сетевого протокола. На этом, на самом деле, можно было бы запустить фейерверк и закончить. Да, он облегчает коммуникацию между разработчиками фронта и бэка, даёт лучшую структуру и понимание данных, простоту в разработке, протоколо-независимость, но, главное, с моей точки зрения, именно вышеуказанное отделение.
Что значит это для фронтэндера? Я обычно делаю модуль доступа к API и работаю через него:
import api from '@/api'; api.products.list(); api.users.update(userId, );
Внутри list () и update () запросы через fetch () или axios () к нужным эндпойнтам backend API. Переход на стандарт JSON-RPC несложен, может быть проведен постепенно и особо профита в кодинге не приносит, кроме отсутствия необходимости задумываться, что использовать — POST, PUT или PATCH, и как передавать параметры, как обрабатывать приходящий результат, ошибки и т.п.
Совсем другое дело бэкенд. Я большой фанат CodeIgniter 4 (далее CI), считаю его лучшим PHP фреймворком для малых и средних по размеру API, и буду говорить на его примере, но Laravel, Sping Boot, Django работают примерно по тому же принципу.
На бэке каждый запрос обрабатывается контроллером (которые сейчас по сути являются уже рудиментом MVC архитектурного шаблона времён генерации контента на сервере), в который передаются (условно) HttpRequest и HttpMessage. Один контроллер может обрабатывать несколько эндпойнтов, это прописывается в роутинге фреймворка. В контроллерах есть доступ к деталям транспортного (HTTP) протокола. Зачастую в контроллерах лежит вся бизнес логика, включая работу с БД и другими внутренними сервисами.

Что происходит, когда вы решаете сменить фреймворк, потому что нашли лучше? Вы переписываете контроллеры. Что происходит, когда ваш фреймворк радикально обновился, и вам нравятся эти новшества, вы хотите их использовать, но там много breaking change изменений? Вы переписываете контроллеры. А это бОльшая часть кода. И перейти на новую версию постепенно довольно сложно, только разом. Всё потому что ваша логика встроена в фреймворк.
А что с JSON-RPC? Там есть только один контроллер, который обрабатывает все запросы и перенаправляет их на нужные модули своим внутренним роутингом. Вот весь HTTP роутинг в CI:
$routes->post('rpc', 'JsonRpcController::index');
Вот файл внутреннего роутинга, используемый JsonRpcController-ом:
[ "transactions:list" => "Users\Transactions::list", "withdrawal:create" => "Users\Transactions::createWithdrawal", ], "utils" => [ "resources:list" => "Utils\Resources::list", "resources:update" => "Utils\Resources::update", "resources:getByKey" => "Utils\Resources::getByKey", "resources:updateByKey" => "Utils\Resources::updateByKey", "resourceCache:clear" => "Utils\Resources::clearCache", ], ]; public static function route($method) < $path = explode('.' , $method); $route = self::$routes; foreach ($path as $step) < $route = $route[$step]; >return $route; > >
Иерархическая модель в $routes задана для удобства. Функция route() берет метод запроса ( method — «плоский» вариант роута, например, users.transactions: list) и выдает соответствующий ему класс и функцию на нем.
Выбор нотации названия метода полностью за разработчиком, но мне нравятся рекомендации Google из вышеприведенной ссылки.
Utils\Resources и Users\Transactions — просто PHP классы, отвечающие за бизнес логику. Им приходят данные в виде объекта, и они отдают результат в виде объекта. Никакой связи с HTTP протоколом. Если потребуется сменить фреймворк, то нужно будет переписать только один файл — JsonRpcController.php
request->getJSON(); > catch (\Throwable $th) < return $this->response->setJSON($this->errorResponse(-32700)); > $response = null; try < // batch payload if (is_array($payloadData)) < if (count($payloadData) == 0) < return $this->response->setJSON($this->errorResponse(-32600)); > $response = []; foreach ($payloadData as $payload) < $singleResponse = $this->processRequest($payload); if ($singleResponse != null) < $response[] = $singleResponse; >> if (count($response) > 0) < return $this->response->setJSON($response); > // single request > else if (is_object($payloadData)) < $response = $this->processRequest($payloadData); return $this->response->setJSON($response); > else < return $this->response->setJSON($this->errorResponse(-32700)); > > catch (\Throwable $th) < return $this->response->setJSON($this->errorResponse(-32603, null, [ "msg" => $th->getMessage(), "trace" => $th->getTrace() ])); > > /** * Process single JSON-RPC request. * * @param object $paylod Request object * @return object Response object */ private function processRequest($payload) < if (!is_object($payload)) < return $this->errorResponse(-32700); > if (!property_exists($payload, "jsonrpc") && !property_exists($payload, "method")) < return $this->errorResponse(-32600); > $payload->context = new stdClass(); if ($this->request->currentUser ?? NULL) < $payload->context->user = $this->request->currentUser; > $route = JsonRpcRoutes::route($payload->method); if (!$route) < return $this->errorResponse(-32601, $payload->id); > list($className, $methodName) = explode("::", $route); $className = JsonRpcRoutes::$basePath . $className; $outcome = (new $className())->$methodName($payload); if (!property_exists($payload, "id") || !$outcome) < return null; >$data = [ "jsonrpc" => "2.0", "id" => $payload->id ]; return array_merge($data, $outcome); > /** * Used for generic failures. * * @param int $errorCode according to JSON-RPC specification * @return Object Response object for this error */ private function errorResponse($errorCode, $id = null, $data = null) < $response = [ "jsonrpc" =>"2.0", "error" => [ "code" => $errorCode, "message" => '' ], "id" => $id ]; if ($data) < $response["error"]["data"] = $data; >switch ($errorCode) < case '-32600': $response["error"]["message"] = "Invalid Request"; break; case '-32700': $response["error"]["message"] = "Parse error"; break; case '-32601': $response["error"]["message"] = "Method not found"; break; case '-32602': $response["error"]["message"] = "Invalid params"; break; case '-32603': $response["error"]["message"] = "Internal error"; break; default: $response["error"]["message"] = "Internal error"; break; >return $response; > >
Контроллер мог бы быть в три-четыре раза меньше, но мне захотелось реализовать требования спецификации JSON-RPC 2.0 по максимуму, в итоге половина кода — корректная обработка всевозможных ошибок. По сути же он просто берет роут из JsonRpcRoutes и вызывает соответствующий метод на нужном классе, передавая ему параметры.

Да, в бэкэнд фреймворках есть другие часто используемые сервисы — например, для упрощения доступа к данным в БД (Active records (не ORM) в CodeIgniter 4 — очень эффектное решение, генерирующее эффективный SQL, возможно, единственное, ради чего стоит использовать CI, а не чистый rpc.php как вход с вебсервера), и их надо будет адаптировать при возможном переезде, но при желании модуль бизнес-логики (Utils\Resources и Users\Transactions) можно написать на чистом PHP/Java/Python, либо самостоятельно использовать сторонние библиотеки и быть полностью независимым от фреймворка. А ведь фреймворки очень хотят привязать разработчиков к себе.
А насколько проще тестировать свои классы, а не контроллеры, насколько проще строить архитектуру приложения без ограничений фреймворка, его контекста, потока исполнения и магии.
Я уж не говорю о переиспользовании кода: вызвать из одного контроллера метод другого, чтобы применить его ответ для выдачи клиенту — задача не тривиальная. Потому что вход и выход в контроллерах — через протоколозависимые шлюзы фреймворка. Да, можно вытащить всю логику из контроллеров и использовать их как пустые обертки/прокси -, но в чем тогда будет их смысл? Да и именно это и делает JSON-RPC, — в том числе.
Нынешние бэкенд фреймворки создавались когда всё еще генерировалось на сервере и клиенту посылался готовый html. Для всего этого нужны были серверные сессии, шаблонизаторы и прочие штуки серверных фреймворков. Для современных SPA-ориентированных бэкенд APIs всё это не нужно, это тяжелый груз, который часто как кандалы мешает работать с современными технологиями.
Вывод
JSON-RPC дарит разработчикам свободу.

К сожалению JSON-RPC всё еще относится к эзотерическому знанию, крутость которого понимаешь только после того как его попробуешь, а попробовать решаются только разработчики, достигшие определенного уровня усталости от жизни такой. Большинство продолжает самоделить каждый раз корявые рестоподобные велосипеды.
Я бы даже сказал, что JSON-RPC — единственный стандарт, который технически полностью можно реализовать при построении коммуникации с бэкендом в 90% современных web app и мобильных приложений (если не смотреть в сторону gRPC и экзотики). И который реально приносит пользу в DX. Попытка реализации других стандартов будет большим компромиссом (просто оставлю здесь слово «HATEOAS»).
Во второй части я остановлюсь на нескольких особенностях при работе с JSON-RPC, а именно: зачем нужны batch пакеты, аутентификация и авторизация, как видеть в DevTools / Network и логах вебсервера семантически понятную информацию о запросах, а не просто /rpc , и как стать господином бэкэнд фреймворка, а не его рабом.