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.1 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
endEndpointGroup
# app/api/endpoint_groups/users_group.rb
class UsersGroup < RailsNinja::EndpointGroup
tags "Users"
ninja_headers "X-Request-ID"
include_endpoint ListUsers
endGroups 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"
endMount the API in Rails:
# config/routes.rb
Rails.application.routes.draw do
mount ApplicationApi => "/api"
endRails 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: []
endFields are required by default. Available scalar types are String, Int,
Float, Boolean, and File 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.
File uploads
schema :DocumentIn do
field :title, RailsNinja::Types::String
field :attachments, [RailsNinja::Types::File]
field :metadata, DocumentMetadata, required: false
endA request schema with a File field is documented as multipart/form-data
and its values arrive as ActionDispatch::Http::UploadedFile. Arrays of files
accept both attachments[] and repeated bare attachments parts, which is what
OpenAPI generated clients send. Nested schema fields may be sent as JSON
strings in their own part; they are validated with JSON types, not form
coercion. File fields are only supported at the top level of a request
schema and cannot appear in responses.
Schemas may also be standalone:
class ItemOut < RailsNinja::Schema::Base
field :id, RailsNinja::Types::Int
field :name, RailsNinja::Types::String
endUse one_of for polymorphic response fields and OpenAPI schemas:
schema :Pet do
field :animal, one_of(Cat, Dog, discriminator: :kind)
endThe 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))
endresponse: 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
endDocument 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
endOnly 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
endHeaders can also be declared per route:
get "/items", headers: [{ name: "X-Request-ID", required: false }]
def list_items
# ...
endEndpoint-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
endHTTP bearer schemes are also supported:
openapi_security_scheme :UserAuth, type: "http", scheme: "bearer"
openapi_security :UserAuthEndpoint options
Routes accept summary:, tags:, headers:, and deprecated_paths::
get "/items",
summary: "List items",
deprecated_paths: ["/old_items"]
def list_items
# ...
endDeprecated 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
bundle exec rake rails_ninja:openapi:generate OPENAPI_VERSION=3.1.0The default OpenAPI version is 3.2.0. Set OPENAPI_VERSION to a 3.0.x, 3.1.x,
or 3.2.x version when a consumer requires another dialect. 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 testCI tests every compatible combination of Action Pack and Active Support 7.1 through 8.1 with MRI Ruby 3.0 through 4.0. Each lane resolves the latest patch release in its minor series.
License
MIT