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: игнорируем шифрование при авторизации
Каким принципам следовать
проще = лучше
зачем переплачивать за TLS, если можно рискнуть данными
В плохом 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 для начинающих разработчиков.
Комментарии (12)
Kelbon
07.11.2024 14:25Мне всегда казались эти заявления про отсутствие состояния абсурдом
отсутствие состояния;
кэширование;
Противоречие
Сервис не должен хранить информацию между запросами. Все необходимое для обработки запроса он получает прямо из самого запроса.
если всё необходимое уже было в запросе, то зачем его отправлять? На месте посчитай
управление ресурсами
а что за ресурсы? Стейт сервера что ли?) Какая разница клиенту на сервере лежит ресурс или в базе данных ?
lastrix
07.11.2024 14:25Хранение состояния != кеширование.
Пример кеширования:
1. Твитты лежат в редисе, а не БД, если пользователь опять выполнит запрос.
2. Для проверки доступа нужно получить вектор доступа пользователя к ресурсу, его из БД каждый раз брать не надо.
Пример состояния:
1. У вас сессия хранит корзину. Добавляя туда что-то - вы на сервере в сессию пишете новые записи.
2. Мастер настройки "чего-то" из нескольких этапов. Каждый этап хранится в сессии, пока не дойдете до последнего шага.egribanov
07.11.2024 14:25А как правильно сделать корзину? Хранить список товаров допустим в базе данных, для авторизованных и не авторизованных пользователей. Плохие люди могут заспамить базу запросами по идее. Или хранить для не авторизованных пользователей в каком нибудь редис это не считается состоянием?
AstarothAst
07.11.2024 14:25Какие-то очень странные утверждения, если честно...
Та часть, где про "непонятное апи"
Ни один разумный человек не полагается на абстрактную "понятность", он смотрит в документацию. Swagger, OpenAPI и прочие страшные слова придумали как раз для этого.
Что касается HTTP‑методов, в плохом сервисе используются только POST‑запросы
Если б вы написали "в плохом REST-сервисе", то и вопросов бы не было - с этой точки зрения плох любой сервис, который просто не соответствует принципам rest, хотя это уже культ карго какой-то. Но вы написали так, словно гонять все через POST это плохо само по себе, а это не так... Для начала гоняя все одним методом мы избавимся от проблемы, собственно, метода - метод легко отчуждаем от url, и периодически теряется, что приводит к прекрасным часам в попытке наиграть ошибку клиента, что бы в итоге понять, что вы с ним один и тот же url дергаете по-разному, вы у себя постом, а он патчем - глупо и достадно, но случается повсеместно. Если все гонять через POST, то значение начинает иметь только url и его тело, и напортачить тут уже гораздо сложнее - любой потерянный кусочек данных тут же себя проявит еще на подлете.
Во-вторых работа через POST уже давно оформилась в отдельный принцип, который окрестили RPC, и куча народа счастливо живет и здравствует им пользуясь, непонятно к чему заявлять, что "это плохо", когда массе народу вполне себе хорошо. Плюс именно такой протокол взаимодействия легко переносится на иные каналы - rabbitmq или вебсокеты, где просто не предусмотрены никакие типы запроса. Url+body туда перенесется вполне естественно и непринужденно, а вот все эти GET, PATCH и прочие DELETE уже будут выглядеть чужеродно.
Еще одно важное правило для плохого REST‑сервиса — минимизировать количество кодов ответа. Обычно достаточно 2–3 варианта ответа с кодом 200. Например, 200 — запрос дошел, а 500 — ошибка, когда что‑то пошло не так. Что именно произошло? Неважно — логи всегда помогут разобраться.
Кодов ответа несколько десятков, потенциальных состояний системы - бесконечное множество, поэтому код 404 мало что скажет без заглядывания в документацию. То ли страница не найдена, то ли пользователь, то ли у пользователя счет?.. Одно ясно - что-то не нашлось! А что именно - помогут понять логи.
Так же не забываем, что есть огромное множество людей, которые не хотят смешивать бизнеслогику и транспорт, поэтому для них 200 ОК обозначает, что сервис запрос получил и успешно его обработал, а то, что результат обработки - это ошибка, это уже не вопрос протокола взаимодействия, это бизнесовая ошибка, и обрабатывается на другом уровне. Утверждать, что такой подход глупость - глупость.
Плохой REST‑сервис должен хранить состояние
Для решения описанных проблем давным-давно придумана масса решений, от банальной работы через общую БД, в которой хранится то самое общее состояние горизонтально масштабированного сервиса, и до распределенного in-memory кэша навроде hazelcast. Поэтому утверждать, что хранение состояния на сервисе это всегда плохо нельзя - это всегда ведет к проблемам, которые нужно решать, но плохо ли это? Зависит от условий задачи, иногда жить без состояния просто нельзя.
В плохом REST‑сервисе шифрование не нужно
А в хорошем - нужно! Точно? Ну, точно же?! Точно?.. *Падме.jpg* Хммм... А ведь можно терменировать tls/mtls на уровне ingress, а внутри неймспейса кубернетиса гонять обычный http траффик. Я не девопс, поэтому говорю, как умею, основная мысль тут в том, что можно снять с сервиса ответственность за все эти приседания с сертификатами, и перенести их на инфраструктурный уровень - они останутся, просто будут в другом месте, и их точно не будет в нашем сервисе. Плохо что ли? Хорошо же!
В общем странная, очень странная статья. Зато с фотографией Кости Латышова, который точно знает, что нам не стоит нести на прод!
DieSlogan
07.11.2024 14:25Однажды очень долго не мог понять, почему сервис выдаёт 403 ошибку. В конце концов выяснилось, что до сервиса запрос не доходил, а рубился каким-то роутером.
scome
07.11.2024 14:25Еще часто (в моей практике было трижды) на проде девопсы/админы просто на уровне nginx закрывали все запросы, кроме GET/POST. В итоге, сделав все «красиво» выходишь в прод и ловишь проблемы во многом из-за своих стремлений к прекрасному.
Лучшее, действительно, часто враг хорошего.
VoodooCat
07.11.2024 14:25Сервис который не хранит состояние между запросами - это или простой шлюз или статический контент. Любой полезных сервис, разумеется, с точки зрения, пользователя - хранит состояние за которым клиент и обращается через API.
Вы можете поставить хоть 100 stateless "сервисов", но масштабировать это никак не поможет, полезные данные и львиная часть работы - скорее всего будет на БД. Если же сервер занят на 100% перекладыванием из DTO в DTO и сериализацией - то видимо надо лечить сервис. Есть сервисы, с тяжелой нагрузкой на CPU и IO, допустим это Git-хостинг: стоит ли объяснять что узкими местами являются CPU и хранилище которое нужно как-то сначала расшарить между узлами в кластере, обеспечить не противоречивый доступ к хранилищу и т.п. Это всё - и есть состояние, которое http-frontend-ом никак (сервисом) - не масштабируется.
Пример с аутентификацией и вовсе надуманный: кластеризация с учетом сессий решена еще в прошлом веке: кто дискриминирует по IP, кто на прикладном уровне. Однако, аутентификация (!) никак не связана с сессией, и тем более сессия ничего не говорит про авторизацию. Авторизация - это акт проверки документов, она не может полагаться на мистические сессии, оно просто не нужно. А аутентификация - акт предъявления документов. Сессия - ассоциация каких-то данных с сеансом пользования услугой, в рамках одного соединения, или в рамках одной сессии браузера, или в рамках многих сессий браузера на протяжении 5 лет. Для того что бы иметь сессию - совсем даже не обязательно пользователя аутентифицировать, он может держать свое состояние у себя или ему может быть выдан идентификатор сессии без аутентификации. И это - тоже всё сессии.
lazy_val
Так все-таки сессии пользователя на стороне backend хранить - это REST или не REST? Или плохой REST? Или что?