Как создать плохой REST-сервис: краткое руководство
REST API — один из самых популярных типов веб‑сервисов. Но несмотря на множество туториалов по его созданию, на практике встречаются сервисы, которые вызывают лишь разочарование у пользователей.
Это подтолкнуло Костю, проектного разработчика в Naumen, создать краткое руководство по написанию плохого REST‑сервиса. Уже несколько лет он занимается поддержкой и развитием проектов на Naumen Service Management Platform, часто сталкивается с проектированием REST API и точно знает, каких ошибок лучше не допускать.
Старший разработчик Naumen Service Desk
В статье Костя поделился основными антипаттернами и рассказал, что не нужно нести на прод.
REST API: что важно знать
REST API — это архитектурный стиль, который определяет, как сервер должен организовать взаимодействие с клиентом. Хотя многие путают его с HTTP, REST — это не протокол передачи данных. Сервис может работать через HTTP, но это не делает его REST API.
Чтобы называться REST API, сервис должен соответствовать определенным требованиям к архитектуре:
клиент-серверная модель;
отсутствие состояния;
кэширование;
единообразие интерфейса;
многоуровневая система;
код по требованию (необязательно);
управление ресурсами (обращаемся не к сервису, а к ресурсу).
Для себя я выделяю два ключевых требования к REST API. Первое — это отсутствие состояния. Сервис не должен хранить информацию между запросами. Все необходимое для обработки запроса он получает прямо из самого запроса. Второе — это правильное управление ресурсами. Важно, как формируются URL, какие HTTP‑методы используются и как структурированы ответы. Это напрямую влияет на удобство работы с сервисом.
Согласно такому подходу следует использовать существительные в URL для обозначения ресурсов и глаголы в HTTP‑методах, таких как GET или POST, это поможет клиентам быстрее интегрироваться с сервисом и избежать путаницы.
Как определить хороший REST-сервис
Прежде чем говорить о том, как сделать плохой сервис, рассмотрим, несколько важных характеристик для REST‑сервиса, которые помогут понять, что перед нами хороший сервис:
Понятность. Клиент должен легко понять, как обращаться к сервису, и что он получит в ответ.
Соответствие REST‑принципам. Сервис должен придерживаться принципов REST, чтобы клиенту было понятно, как с ним работать.
Безопасность. Доступ к сервису должен быть ограничен, чтобы предотвратить утечку данных и несанкционированный доступ к конфиденциальной информации.
Производительность. Запросы должны обрабатываться быстро, без длительных задержек.
Вредные советы: как не нужно делать REST-сервис
Теперь, когда мы обсудили, как должен выглядеть хороший REST‑сервис, рассмотрим, что нужно сделать, чтобы он стал «плохим». Ниже делюсь четырьмя вредными советами.
Вредный совет № 1: разрушаем управление ресурсами
Каким принципам следовать
URL должен подробно описывать, что клиент хочет сделать — чем запутаннее и длиннее, тем лучше:)
для всех операций используем POST
если запрос дошел, отправляем 200 ОК — он же дошел, значит все в порядке
В плохом REST‑сервисе URL не должен быть привязан к каким‑либо ресурсам. В идеале, он должен просто описывать, что произойдет в результате обработки запроса. Так клиент «поймет», что делать и что будет происходить. Правда, есть обратная сторона — легко сделать опечатку в таком URL, но, поскольку мы делаем плохой сервис, это не столь критично.
Что касается HTTP‑методов, в плохом сервисе используются только POST‑запросы. Ведь POST позволяет передавать тело запроса, и там уже можно указать все, что нужно для его обработки.
Еще одно важное правило для плохого REST‑сервиса — минимизировать количество кодов ответа. Обычно достаточно 2–3 варианта ответа с кодом 200. Например, 200 — запрос дошел, а 500 — ошибка, когда что‑то пошло не так. Что именно произошло? Неважно — логи всегда помогут разобраться.
Пример плохого API — это API Twitter v 1.0, которое прекратило свое существование в 2007 году. Многим оно запомнилось как неудобное и запутанное. В документации было четыре метода, и никто толком не понимал, что они делают:
Первый метод возвращал все твиты пользователя.
Второй, несмотря на название «update», позволял создавать новые твиты.
Третий метод удалял твиты.
Четвертый возвращал ленту твитов для авторизованного пользователя.
С тех пор Twitter перешел на версию 2 API, которая работает как нормальный REST‑сервис, с понятной и интуитивной структурой.
Вредный совет № 2: забиваем на хранение состояния
Каким принципам следовать
Сохраняйте состояние запросов. Пусть сервис «запоминает» все:
Плохой REST‑сервис должен хранить состояние. Один из примеров — это сессии. Авторизуемся, получаем идентификатор сессии, сохраняем его в cookie, и дальше сервис уже «знает» нас. Все последующие запросы проходят без необходимости повторной авторизации. С одной стороны, это удобно, ведь клиенту не нужно каждый раз отправлять данные для авторизации. С другой стороны, это создает массу проблем при масштабировании и безопасности.
Предположим, количество пользователей сервиса увеличилось, и мы решили добавить второй REST‑сервис для распределения нагрузки. Настроим балансировку запросов так, что четные запросы будут попадать на один инстанс, а нечетные — на другой. Проблема в том, что если сессии не реплицируются между серверами, пользователь будет авторизован только на одном из них.
Это может привести к непредсказуемому поведению и увеличению времени отклика, когда сервер сообщает, что пользователь не авторизован, хотя буквально несколько минут назад успешно прошел авторизацию на другом сервере.
Вредный совет № 3: игнорируем шифрование при авторизации
Каким принципам следовать
проще = лучше
В плохом REST‑сервисе шифрование не нужно. Зачем тратить деньги на сертификаты и удостоверяющие центры? Можно использовать простейшую авторизацию — Basic Authentication, при которой имя пользователя и пароль передаются в заголовке запроса, закодированные в Base64. Шифрование отсутствует, но кажется, что сервис защищен. Однако есть нюанс: если злоумышленник перехватит трафик, он легко сможет расшифровать данные.
Пример: пользователь работает из публичной сети, а злоумышленник использует приложение для перехвата трафика, такое как Wireshark. Он видит наши креды и использует их для несанкционированного доступа к сервису.
Что видит злоумышленник, когда шифрование отсутствует
Если бы мы применили шифрование, например, через TLS, данные были бы защищены, и злоумышленник увидел бы лишь зашифрованный набор символов.
Что видит злоумышленник, когда шифрование есть
Вредный совет № 4: блокируем потоки
Каким принципам следовать
пользователь любит смотреть на красный индикатор загрузки
блокировка потоков — способ почувствовать себя настоящим программистом
асинхронность для слабаков
Блокировка потоков напрямую влияет на производительность сервиса. Вот, о чем важно помнить:
Масштабирование
Проблемы с производительностью могут возникнуть, когда запросы начинают обрабатываться медленнее. Это происходит, когда сервис начинает деградировать из‑за увеличения количества пользователей. В таких случаях помогает масштабирование — можно добавить еще один инстанс в кластер для распределения нагрузки. Таким образом, нагрузка балансируется между инстансами, что улучшает общую производительность.
Асинхронное выполнение операций
Еще одна распространенная ситуация — сервис отправляет данные в смежный, внутренний или внешний, сервис, который обрабатывает запрос слишком долго. В таких случаях имеет смысл сделать операцию асинхронной. Это значит, что клиент получит ответ о том, что операция принята к исполнению, а сама задача выполнится позже, например, через очередь. Можно вернуть клиенту идентификатор для отслеживания выполнения операции. Если же отслеживание не требуется — как в случае с отправкой уведомлений или писем — можно вообще не возвращать ничего.
Неблокирующие вызовы
Производительность может также ухудшиться, если используются блокирующие вызовы. Потоки, обрабатывающие HTTP‑запросы, имеют ограниченное количество. Если они все заняты, возникает ситуация, когда «закончились потоки». Чтобы этого избежать, важно использовать неблокирующие вызовы, которые освобождают потоки для других операций.
Но для плохого REST‑сервиса блокирующие вызовы — то, что нужно. Все HTTP‑потоки будут заняты выполнением задач, а в это время пользователь будет смотреть на индикатор загрузки. Для этого стоит использовать блокирующие методы, которые занимают потоки и заставляют другие запросы ждать своей очереди.
Чем отличаются методы blocking и non‑blocking? Оба используют по два потока для обработки запросов. Однако метод non‑blocking имеет дополнительные четыре потока — executor threads, которые выполняют основную работу, освобождая HTTP‑потоки для других запросов.
При использовании метода blocking потоки «засыпают» на 10 секунд, блокируя возможность обработки других запросов в течение этого времени.
В случае с методом non‑blocking HTTP‑поток, обрабатывающий запрос, передает задачу в пул потоков — thread pool, содержащий четыре executor threads. Один из свободных потоков возьмет задачу на выполнение, а HTTP‑поток освободится для обработки новых запросов.
Ошибки в проектировании REST‑сервиса могут серьезно повлиять на его производительность, безопасность и удобство использования. Эти вредные советы наглядно показывают, чего лучше избегать при разработке API, чтобы сделать сервис надежным, удобным и безопасным для пользователей.
Если хотите подробнее разобраться в теме, рекомендую посмотреть запись моего доклада с Naumen Java Junior Meetup для начинающих разработчиков.