Использование 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::HostAuthorization
  • Rack::Sendfile
  • ActionDispatch::Static
  • ActionDispatch::Executor
  • ActionDispatch::ServerTiming
  • ActiveSupport::Cache::Strategy::LocalCache::Middleware
  • Rack::Runtime
  • ActionDispatch::RequestId
  • ActionDispatch::RemoteIp
  • Rails::Rack::Logger
  • ActionDispatch::ShowExceptions
  • ActionDispatch::DebugExceptions
  • ActionDispatch::ActionableExceptions
  • ActionDispatch::Reloader
  • ActionDispatch::Callbacks
  • ActiveRecord::Migration::CheckPending
  • Rack::Head
  • Rack::ConditionalGet
  • Rack::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::CacheStore
  • ActionDispatch::Session::CookieStore
  • ActionDispatch::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::MethodOverride
  • ActionDispatch::Cookies
  • ActionDispatch::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::ControllerMethods
    • ActionController::HttpAuthentication::Digest::ControllerMethods
    • ActionController::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
    end

    Rails не передаёт эту конфигурацию автоматически.

Лучшее место для добавления модуля — в вашем ApplicationController, но также можно добавлять модули в отдельные контроллеры.

On this page