Rails Ninja
Rails Ninja is a small Rails API framework inspired by Django Ninja. It provides a route DSL, schema-based request validation and response serialization, and generated OpenAPI documentation.
Rails Ninja requires Ruby 3 or newer and Rails 7 or newer.
Installation
gem "rails_ninja"
Then run bundle install.
Structure
Rails Ninja has three building blocks:
RailsNinja::APIis the root Rack application and OpenAPI document.RailsNinja::EndpointGroupgroups related routes under a prefix and tag.RailsNinja::Endpointkeeps one endpoint and its schemas in a standalone class.
An API can define routes inline, include standalone Endpoints, and mount EndpointGroups. An API cannot mount another API: mount independent APIs separately in Rails when they need separate documentation.
Endpoint
# app/api/endpoints/list_users.rb
class ListUsers < RailsNinja::Endpoint
schema :UserOut do
field :id, RailsNinja::Types::Int
field :name, RailsNinja::Types::String
field :email, RailsNinja::Types::String
end
get "/", response: [UserOut]
def handle
User.all
end
end
EndpointGroup
# app/api/endpoint_groups/users_group.rb
class UsersGroup < RailsNinja::EndpointGroup
"Users"
ninja_headers "X-Request-ID"
include_endpoint ListUsers
end
Groups may also define routes directly or mount other EndpointGroups.
API
# app/api/application_api.rb
class ApplicationApi < RailsNinja::API
title "My Service"
version "1.0"
mount UsersGroup, prefix: "/users"
end
Mount the API in Rails:
# config/routes.rb
Rails.application.routes.draw do
mount ApplicationApi => "/api"
end
Rails Ninja adds app/api to Rails' autoload and eager-load paths. The example
exposes:
GET /api/usersGET /api/openapi.jsonGET /api/docs
The endpoint verbs are get, post, put, patch, and delete. A
verb declaration applies to the method defined immediately after it.
Schemas
schema :ItemIn do
field :name, RailsNinja::Types::String
field :price, RailsNinja::Types::Float
field :active, RailsNinja::Types::Boolean, required: false, default: true
field :tags, [RailsNinja::Types::String], required: false, default: []
end
Fields are required by default. Available scalar types are String, Int,
Float, and Boolean under RailsNinja::Types. A field may also contain a
nested schema or a one-element array of a scalar or schema.
JSON input is strictly type-checked. Canonical path, query, and form values are
decoded first, so an integer query value such as "20" becomes 20. Invalid
requests return 422 with an errors array, and validated values are merged
into params as symbol keys.
For GET and DELETE, a request schema is read from and documented as query
parameters. POST, PUT, and PATCH use a request body.
Schemas may also be standalone:
class ItemOut < RailsNinja::Schema::Base
field :id, RailsNinja::Types::Int
field :name, RailsNinja::Types::String
end
Use one_of for polymorphic response fields and OpenAPI schemas:
schema :Pet do
field :animal, one_of(Cat, Dog, discriminator: :kind)
end
The discriminator is optional. Each variant needs a default value for its discriminator field to appear in the OpenAPI mapping.
Requests and responses
post "/items", request: ItemIn, response: ItemOut
def create_item
Item.create!(params.slice(:name, :price, :active, :tags))
end
response: ItemOut serializes one object; response: [ItemOut] serializes a
collection. Without a response schema, a normal return value is not rendered.
Use render_json or head for explicit responses:
get "/health"
def health
render_json({ status: "ok" })
end
delete "/items/:id"
def delete_item
Item.find(params[:id]).destroy!
head 204
end
Document multiple statuses with responses::
get "/items/:id", responses: { 200 => ItemOut, 404 => ErrorOut }
def show_item
item = Item.find_by(id: params[:id])
return render_json({ error: "Not found" }, status: 404) unless item
item
end
Only the 200 schema is serialized automatically. Other statuses must be
committed with render_json or head.
Callbacks, headers, and tags
Before actions run from the API through the matched group branch to the
Endpoint. They may halt processing with head or render_json:
class InternalApi < RailsNinja::API
before_action :authenticate!
ninja_headers "X-API-Key"
def authenticate!
head 401 unless valid_api_key?(request.headers["X-API-Key"])
end
end
Headers can also be declared per route:
get "/items", headers: [{ name: "X-Request-ID", required: false }]
def list_items
# ...
end
Endpoint-level headers override class-level headers with the same name. Tags on
an EndpointGroup apply to its included Endpoints and determine their Swagger UI
group and operationId prefix.
OpenAPI authorization
Declare security metadata on the root API. Runtime authentication remains the responsibility of a before action.
class InternalApi < RailsNinja::API
openapi_security_scheme(
:ApiKeyAuth,
type: "apiKey",
in: "header",
name: "X-API-Key"
)
openapi_security :ApiKeyAuth
end
HTTP bearer schemes are also supported:
openapi_security_scheme :UserAuth, type: "http", scheme: "bearer"
openapi_security :UserAuth
Endpoint options
Routes accept summary:, tags:, headers:, and deprecated_paths::
get "/items",
summary: "List items",
deprecated_paths: ["/old_items"]
def list_items
# ...
end
Deprecated paths remain routable and are marked as deprecated in OpenAPI.
Set server "https://api.example.com" on an API to declare its server URL, or
docs false to disable /docs and /openapi.json.
Static OpenAPI files
Generate an OpenAPI 3.2 JSON file for every API:
bundle exec rake rails_ninja:openapi:generate
bundle exec rake rails_ninja:openapi:generate OUTPUT=docs/api
The default output directory is public/openapi. File names come from the API
class name, such as PublicApi to public_api.json.
Development
Install dependencies and run the test suite:
bundle install
bundle exec rake test
CI tests every compatible combination of Action Pack and Active Support 7.0 through 8.1 with MRI Ruby 3.0 through 4.0. Each lane resolves the latest patch release in its minor series.
License
MIT