Использование Rails для API-приложений
В этом руководстве вы узнаете, что предоставляет Rails для API-приложений, как настроить Rails для работы без браузерных функций, какие middleware и модули контроллера выбрать.
В этом руководстве вы узнаете:
- Что предоставляет Rails для API-приложений.
- Как настроить Rails для запуска без каких-либо браузерных функций.
- Как решить, какие middleware вы хотите включить.
- Как решить, какие модули использовать в вашем контроллере.
Что такое API-приложение?
Обычно, когда говорят, что Rails используется как «API», имеют в виду предоставление программно доступного API вместе с веб-приложением. Например, GitHub предоставляет API, который можно использовать из ваших собственных клиентов.
С приходом клиентских фреймворков всё больше разработчиков используют Rails для создания бэкенда, общего для веб-приложения и других нативных приложений.
Например, X использует свой публичный API в своём веб-приложении, которое построено как статичный сайт, потребляющий JSON-ресурсы.
Вместо использования Rails для генерации HTML, который взаимодействует с сервером через формы и ссылки, многие разработчики относятся к своему веб-приложению как просто к API-клиенту, доставляемому в виде HTML с JavaScript, потребляющим JSON API.
Это руководство охватывает создание Rails-приложения, отдающего JSON-ресурсы клиенту API, включая клиентские фреймворки.
Зачем использовать Rails для JSON API?
Первый вопрос, который многие люди задают, когда задумываются о создании JSON API с помощью Rails: «Не избыточно ли использовать Rails только для того, чтобы отдавать JSON? Не лучше ли использовать что-то вроде Sinatra?».
Для очень простых API это может быть так. Однако даже в очень HTML-нагруженных приложениях большая часть логики приложения живёт вне слоя представления.
Причина, по которой большинство людей используют Rails, в том, что он предоставляет набор настроек по умолчанию, который позволяет разработчикам быстро начать работу, не принимая множества тривиальных решений.
Давайте посмотрим на некоторые вещи, которые Rails предоставляет из коробки и которые всё ещё применимы к API-приложениям.
Обрабатывается на уровне middleware:
- Перезагрузка: приложения Rails поддерживают прозрачную перезагрузку. Это работает, даже если ваше приложение становится большим, и перезапускать сервер для каждого запроса становится нежизнеспособно.
- Режим development: приложения Rails имеют разумные настройки по умолчанию для разработки, делая её приятной без ущерба производительности в production.
- Режим test: то же, что и режим development.
- Логирование: приложения Rails логируют каждый запрос с уровнем детализации, подходящим текущему режиму. Логи Rails в development включают информацию о среде запроса, запросах к базе данных и базовую информацию о производительности.
- Безопасность: Rails обнаруживает и пресекает атаки IP-спуфинга, а также обрабатывает криптографические подписи способом, защищённым от атак по времени. Не знаете, что такое атака IP-спуфинга или атака по времени? Вот именно.
- Парсинг параметров: хотите указать ваши параметры как JSON вместо URL-encoded строки? Без проблем. Rails декодирует JSON за вас и сделает его доступным в
params. Хотите использовать вложенные URL-encoded параметры? Это тоже работает. - Условные GET-запросы: Rails обрабатывает условные
GET(ETagиLast-Modified), читая заголовки запроса и возвращая правильные заголовки отклика и код состояния. Всё, что вам нужно сделать, — использовать проверкуstale?в контроллере, и Rails позаботится обо всех HTTP-деталях. - HEAD-запросы: Rails прозрачно преобразует
HEAD-запросы вGETи возвращает только заголовки на выходе. Это делаетHEADнадёжно работающим во всех API Rails.
Хотя вы, конечно, могли бы реализовать это самостоятельно с помощью существующих Rack middleware, этот список демонстрирует, что стандартный стек middleware Rails представляет большую ценность, даже если вы «просто генерируете JSON».
Обрабатывается на уровне Action Pack:
- Ресурсный роутинг: если вы строите RESTful JSON API, вам захочется использовать роутер Rails. Чистое и общепринятое сопоставление от HTTP к контроллерам означает, что не нужно тратить время на размышления о том, как смоделировать ваш API в терминах HTTP.
- Генерация URL: обратная сторона роутинга — генерация URL. Хороший API на базе HTTP включает URL (см. в качестве примера GitHub Gist API).
- Отклики с заголовками и редиректами:
head :no_contentиredirect_to user_url(current_user)бывают очень удобны. Конечно, вы могли бы добавить заголовки отклика вручную, но зачем? - Кэширование: Rails предоставляет кэширование страниц, экшенов и фрагментов. Кэширование фрагментов особенно полезно при создании вложенных JSON-объектов.
- Базовая, дайджест и токен-аутентификация: Rails поставляется с готовой поддержкой трёх видов HTTP-аутентификации.
- Инструментирование: в Rails есть API инструментирования, который запускает зарегистрированные обработчики для множества событий, таких как обработка экшена, отправка файла или данных, перенаправление и запросы к базе данных. Полезная нагрузка каждого события несёт соответствующую информацию (для события обработки экшена полезная нагрузка включает контроллер, экшен, параметры, формат запроса, метод запроса и полный путь запроса).
- Генераторы: часто удобно сгенерировать ресурс и получить модель, контроллер, заготовки тестов и маршруты одной командой для дальнейшей доработки. То же касается миграций и прочего.
- Плагины: многие сторонние библиотеки поставляются с поддержкой Rails, что снижает или устраняет затраты на настройку и склейку библиотеки с веб-фреймворком. Это включает такие вещи, как переопределение генераторов по умолчанию, добавление задач Rake и уважение к решениям Rails (например, к логгеру и бэкенду кэша).
Конечно, процесс загрузки Rails также склеивает воедино все зарегистрированные компоненты. Например, процесс загрузки Rails — это то, что использует ваш файл config/database.yml при настройке Active Record.
Короткая версия: возможно, вы не задумывались, какие части Rails остаются применимыми, даже если убрать слой представления, но ответ — большая их часть.
Базовая конфигурация
Если вы создаёте Rails-приложение, которое в первую очередь будет API-сервером, можно начать с более ограниченного подмножества Rails и добавлять функции по мере необходимости.
Создание нового приложения
Можно сгенерировать новое api-приложение Rails:
$ rails new my_api --apiЭто сделает три основные вещи:
- Сконфигурирует ваше приложение для запуска с более ограниченным набором middleware, чем обычно. В частности, оно по умолчанию не будет включать middleware, в основном полезные для браузерных приложений (например, поддержку кук).
- Сделает
ApplicationControllerнаследующимся отActionController::APIвместоActionController::Base. Как и в случае с middleware, это исключит все модули Action Controller, предоставляющие функциональность, в основном используемую браузерными приложениями. - Сконфигурирует генераторы, чтобы они пропускали генерацию вью, хелперов и ассетов при создании нового ресурса.
Генерация нового ресурса
Чтобы посмотреть, как наше только что созданное API справляется с генерацией нового ресурса, давайте создадим ресурс Group. У каждой группы будет имя.
$ bin/rails g scaffold Group name:stringПеред тем как мы сможем использовать наш сгенерированный код, нужно обновить схему базы данных.
$ bin/rails db:migrateТеперь, открыв GroupsController, мы должны заметить, что в API-приложении Rails мы отрисовываем только данные JSON. В экшене index мы запрашиваем Group.all и присваиваем результат переменной экземпляра @groups. Передача её в render с опцией :json автоматически отрисует группы как JSON.
# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
before_action :set_group, only: %i[ show update destroy ]
# GET /groups
def index
@groups = Group.all
render json: @groups
end
# GET /groups/1
def show
render json: @group
end
# POST /groups
def create
@group = Group.new(group_params)
if @group.save
render json: @group, status: :created, location: @group
else
render json: @group.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /groups/1
def update
if @group.update(group_params)
render json: @group
else
render json: @group.errors, status: :unprocessable_entity
end
end
# DELETE /groups/1
def destroy
@group.destroy
end
private
# Использовать колбэки для совместного использования общей настройки или ограничений между экшенами.
def set_group
@group = Group.find(params[:id])
end
# Разрешить только список доверенных параметров.
def group_params
params.expect(group: [:name])
end
endНаконец, мы можем добавить несколько групп в нашу базу данных из консоли Rails:
irb> Group.create(name: "Rails Founders")
irb> Group.create(name: "Rails Contributors")С данными в приложении можно запустить сервер и открыть http://localhost:3000/groups.json, чтобы увидеть наши данные в JSON.
[
{"id":1, "name":"Rails Founders", "created_at": ...},
{"id":2, "name":"Rails Contributors", "created_at": ...}
]Изменение существующего приложения
Если вы хотите взять существующее приложение и сделать его API-приложением, выполните следующие шаги.
В config/application.rb добавьте следующую строчку в начало определения класса Application:
config.api_only = trueВ config/environments/development.rb установите config.debug_exception_response_format, чтобы настроить формат, используемый в откликах, когда в режиме development происходят ошибки.
Чтобы отрисовать HTML-страницу с отладочной информацией, используйте значение :default.
config.debug_exception_response_format = :defaultЧтобы отрисовать отладочную информацию с сохранением формата отклика, используйте значение :api.
config.debug_exception_response_format = :apiПо умолчанию config.debug_exception_response_format установлен в :api, когда config.api_only установлен в true.
Наконец, в app/controllers/application_controller.rb вместо:
class ApplicationController < ActionController::Base
endиспользуйте:
class ApplicationController < ActionController::API
endВыбор middleware
API-приложение поставляется со следующими middleware по умолчанию:
ActionDispatch::HostAuthorizationRack::SendfileActionDispatch::StaticActionDispatch::ExecutorActionDispatch::ServerTimingActiveSupport::Cache::Strategy::LocalCache::MiddlewareRack::RuntimeActionDispatch::RequestIdActionDispatch::RemoteIpRails::Rack::LoggerActionDispatch::ShowExceptionsActionDispatch::DebugExceptionsActionDispatch::ActionableExceptionsActionDispatch::ReloaderActionDispatch::CallbacksActiveRecord::Migration::CheckPendingRack::HeadRack::ConditionalGetRack::ETag
Смотрите раздел о внутренних middleware руководства по Rack для получения дополнительной информации о них.
Другие плагины, включая Active Record, могут добавлять дополнительные middleware. В общем случае эти middleware независимы от типа приложения, которое вы создаёте, и имеют смысл в API-приложении Rails.
Можно получить список всех middleware в вашем приложении через:
$ bin/rails middlewareИспользование Rack::Cache
При использовании с Rails Rack::Cache использует хранилище кэша Rails для своих entity- и meta-хранилищ. Это означает, что если вы используете memcache для вашего Rails-приложения, то встроенный HTTP-кэш будет использовать memcache.
Чтобы использовать Rack::Cache, сначала нужно добавить гем rack-cache в Gemfile и установить config.action_dispatch.rack_cache в true. Чтобы включить эту функциональность, вы захотите использовать stale? в вашем контроллере. Вот пример использования stale?.
def show
@post = Post.find(params[:id])
if stale?(last_modified: @post.updated_at)
render json: @post
end
endВызов stale? сравнит заголовок If-Modified-Since в запросе с @post.updated_at. Если заголовок новее последней модификации, этот экшен вернёт отклик "304 Not Modified". В противном случае он отрисует отклик и включит в него заголовок Last-Modified.
Обычно этот механизм используется на индивидуальной основе для каждого клиента. Rack::Cache позволяет нам разделять этот механизм кэширования между клиентами. Можно включить межклиентское кэширование в вызове stale?:
def show
@post = Post.find(params[:id])
if stale?(last_modified: @post.updated_at, public: true)
render json: @post
end
endЭто означает, что Rack::Cache будет сохранять значение Last-Modified для URL в кэше Rails и добавлять заголовок If-Modified-Since ко всем последующим входящим запросам к тому же URL.
Воспринимайте это как кэширование страниц с использованием семантики HTTP.
Использование Rack::Sendfile
Когда вы используете метод send_file внутри контроллера Rails, он устанавливает заголовок X-Sendfile. Rack::Sendfile отвечает за фактическую отправку файла.
Если ваш фронтенд-сервер поддерживает ускоренную отправку файлов, Rack::Sendfile переложит работу по фактической отправке файла на фронтенд-сервер. Это позволяет Rails завершить обработку запроса и освободить ресурсы раньше.
Можно настроить имя заголовка, который ваш фронтенд-сервер использует для этой цели, через config.action_dispatch.x_sendfile_header в конфигурационном файле соответствующего окружения.
Можно подробнее узнать о том, как использовать Rack::Sendfile с популярными фронтендами, в документации Rack::Sendfile.
Вот несколько значений для этого заголовка для некоторых популярных серверов, после того как эти серверы будут настроены для поддержки ускоренной отправки файлов:
# Apache и lighttpd
config.action_dispatch.x_sendfile_header = "X-Sendfile"
# Nginx
config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"Убедитесь, что настроили ваш сервер для поддержки этих опций согласно инструкциям в документации Rack::Sendfile.
Использование ActionDispatch::Request
ActionDispatch::Request#params примет параметры от клиента в формате JSON и сделает их доступными в вашем контроллере внутри params.
Чтобы использовать это, вашему клиенту нужно сделать запрос с параметрами, закодированными в JSON, и указать Content-Type как application/json.
Вот пример:
fetch('/people', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ person: { firstName: 'Yehuda', lastName: 'Katz' } })
}).then(response => response.json())ActionDispatch::Request увидит Content-Type, и ваши параметры будут:
{ person: { firstName: "Yehuda", lastName: "Katz" } }Использование middleware для сессий
Следующие middleware, используемые для управления сессией, исключены из API-приложений, так как обычно им не нужны сессии. Если один из ваших API-клиентов — браузер, возможно, вы захотите вернуть один из них:
ActionDispatch::Session::CacheStoreActionDispatch::Session::CookieStoreActionDispatch::Session::MemCacheStore
Хитрость их добавления обратно в том, что по умолчанию им передаются session_options при добавлении (включая ключ сессии), поэтому нельзя просто добавить инициализатор session_store.rb, добавить use ActionDispatch::Session::CookieStore и получить нормально работающие сессии. (Чтобы было понятно: сессии могут работать, но ваши опции сессии будут игнорироваться — т.е. ключ сессии будет по умолчанию _session_id).
Вместо инициализатора вам нужно будет установить соответствующие опции где-то до того, как ваш middleware будет собран (например, в config/application.rb), и передать их в предпочитаемый middleware, например так:
# Это также конфигурирует session_options для использования ниже
config.session_store :cookie_store, key: "_your_app_session"
# Требуется для всего управления сессиями (независимо от session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_optionsДругие middleware
Rails поставляется с рядом других middleware, которые вы можете захотеть использовать в API-приложении, особенно если один из ваших API-клиентов — браузер:
Rack::MethodOverrideActionDispatch::CookiesActionDispatch::Flash
Любой из этих middleware можно добавить через:
config.middleware.use Rack::MethodOverrideУдаление middleware
Если вы не хотите использовать middleware, который включён по умолчанию в набор middleware API-only, его можно убрать через:
config.middleware.delete ::Rack::SendfileИмейте в виду, что удаление этих middleware удалит поддержку некоторых функций в Action Controller.
Выбор модулей контроллера
API-приложение (использующее ActionController::API) поставляется со следующими модулями контроллера по умолчанию:
ActionController::UrlFor | Делает доступными url_for и подобные хелперы. |
ActionController::Redirecting | Поддержка redirect_to. |
AbstractController::Rendering и ActionController::ApiRendering | Базовая поддержка отрисовки. |
ActionController::Renderers::All | Поддержка render :json и сотоварищей. |
ActionController::ConditionalGet | Поддержка stale?. |
ActionController::BasicImplicitRender | Гарантирует возврат пустого отклика, если явного нет. |
ActionController::StrongParameters | Поддержка фильтрации параметров в сочетании с массовым присваиванием Active Model. |
ActionController::DataStreaming | Поддержка send_file и send_data. |
AbstractController::Callbacks | Поддержка before_action и подобных хелперов. |
ActionController::Rescue | Поддержка rescue_from. |
ActionController::Instrumentation | Поддержка хуков инструментирования, определённых Action Controller (подробности об этом смотрите в руководстве по инструментированию). |
ActionController::ParamsWrapper | Оборачивает хэш параметров во вложенный хэш, так что не приходится указывать корневые элементы, например, при отправке POST-запросов. |
ActionController::Head | Поддержка возврата отклика без содержимого, только заголовки. |
Другие плагины могут добавлять дополнительные модули. Список всех модулей, включённых в ActionController::API, можно получить в консоли rails:
irb> ActionController::API.ancestors - ActionController::Metal.ancestors
=> [ActionController::API,
ActiveRecord::Railties::ControllerRuntime,
ActionDispatch::Routing::RouteSet::MountedHelpers,
ActionController::ParamsWrapper,
... ,
AbstractController::Rendering,
ActionView::ViewPaths]Добавление других модулей
Все модули Action Controller знают о своих зависимых модулях, поэтому можно свободно включать любые модули в ваши контроллеры, и все зависимости будут включены и настроены.
Некоторые распространённые модули, которые вы можете захотеть добавить:
-
AbstractController::Translation: поддержка методов локализацииlи переводаt. -
Поддержка базовой, дайджест или токен HTTP-аутентификации:
ActionController::HttpAuthentication::Basic::ControllerMethodsActionController::HttpAuthentication::Digest::ControllerMethodsActionController::HttpAuthentication::Token::ControllerMethods
-
ActionView::Layouts: поддержка макетов при отрисовке. -
ActionController::MimeResponds: поддержкаrespond_to. -
ActionController::Cookies: поддержкаcookies, включая поддержку подписанных и зашифрованных кук. Требует middleware кук. -
ActionController::Caching: поддержка кэширования вью для API-контроллера. Учтите, что вам нужно вручную указать хранилище кэша внутри контроллера, например так:class ApplicationController < ActionController::API include ::ActionController::Caching self.cache_store = :mem_cache_store endRails не передаёт эту конфигурацию автоматически.
Лучшее место для добавления модуля — в вашем ApplicationController, но также можно добавлять модули в отдельные контроллеры.
Треды и выполнение кода в Rails
После прочтения этого руководства вы узнаете: где найти конкурентное выполнение кода в Rails, как интегрировать ручную конкурентность с Rails, как обернуть код приложения с помощью Rails Executor и как повлиять на перезагрузку приложения.
Вносим вклад в Ruby on Rails
Это руководство раскрывает, как ты можешь стать частью продолжающейся разработки Ruby on Rails.