---
title: "Building High Performance Ruby REST APIs with Rage"
description: "Learn how to build Ruby on Rails compatible REST APIs that are fast, modern, and developer-friendly using Rage."
canonicalUrl: "https://zuplo.com/blog/2025/04/13/ruby-rage-rest-api-tutorial"
pageType: "blog"
date: "2025-04-13"
authors: "roman"
tags: "Ruby, API Tooling, Tutorial"
image: "https://cdn.zuplo.com/cdn-cgi/image/fit=crop,width=1200,height=630/www/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image.png"
---
> This article is written by [Roman Samoilov](https://github.com/rsamoilov),
> Tech Lead at SoftServe, and creator of
> [Rage](https://github.com/rage-rb/rage). All opinions expressed are his own.

[Rage](https://github.com/rage-rb/rage) is a Ruby web framework designed for
building fast, modern, and developer-friendly APIs.

With the goal of improving and modernizing the Ruby ecosystem, it tries to
strike a balance between new and existing solutions - Rage provides the same
syntax as Rails while focusing on asynchronous I/O, performance, and effortless
generation of OpenAPI documentation.

In this article, we will use Rage to build a shared Todo list. Users will be
able to add items to this list, mark them as completed, and group the items by
category:

![todo list](/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image-1.png)

Let's walk through the process of building this application, and then we'll
summarize the key takeaways.

## Creating a project

First, we will need to create a new project. Let’s install the gem and set up a
project with an SQLite database:

```bash
$ gem install rage-rb
$ rage new todo-list -d sqlite3
$ cd todo-list
$ bundle install
```

Next, let’s generate our models. The following commands will create both model
and migration files:

```bash
$ rage g model Item
$ rage g model Group
```

_ℹ️ You can see the full list of commands using `rage --tasks`._

We will need to update the models to set up the associations:

```ruby
# app/models/group.rb
class Group < ApplicationRecord
  has_many :items
end

# app/models/item.rb
class Item < ApplicationRecord
  belongs_to :group
end
```

And update the migrations to define the tables:

```ruby
# db/migrate/20250401110130_create_groups.rb
class CreateTodoGroups < ActiveRecord::Migration[8.0]
  def change
    create_table :groups do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end

# db/migrate/20250401110130_create_items.rb
class CreateItems < ActiveRecord::Migration[8.0]
  def change
    create_table :items do |t|
      t.string :content, null: false
      t.boolean :is_completed, null: false, default: false
      t.references :group, index: true, null: false
      t.timestamps
    end
  end
end
```

Finally, let’s seed the database with the first record and create the database:

```ruby
# db/seeds.rb
Item.create!(
  group: Group.new(name: "New Year's resolutions"),
  content: "Build fast APIs with Rage",
  created_at: Time.now.beginning_of_year
)
```

```ruby
$ rage db:create && rage db:migrate && rage db:seed
```

## List API

Next, let’s proceed to creating the list API. We will update our routes:

```ruby
# config/routes.rb
Rage.routes.draw do
  scope path: "api/v1" do
    resources :items, only: %i[index]
  end
end
```

and create the controller class:

```ruby
# app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def index
  end
end
```

Since our Todo items are attached to groups, and we’d like to have structured
JSON responses where each item is nested within a group, we will build our
serializers with [Alba](https://github.com/okuramasafumi/alba). Let’s update the
Gemfile:

```ruby
# Gemfile
gem "alba"
```

We will also set the
[inflector](https://github.com/okuramasafumi/alba?tab=readme-ov-file#inference-configuration)
to enable Alba to automatically determine the correct serializer classes for the
associations:

```ruby
# config/initializers/alba_inflector.rb
Alba.inflector = :active_support
```

Next, let’s create the actual serializers:

```ruby
# app/resources/application_resource.rb
class ApplicationResource
  include Alba::Resource

  root_key :data, :data
  transform_keys :lower_camel
end

# app/resources/group_resource.rb
class GroupResource < ApplicationResource
  attributes :id, :name
  has_many :items
end

# app/resources/item_resource.rb
class ItemResource < ApplicationResource
  attributes :content, is_completed: :Boolean
end
```

Here, we create three classes:

- `ApplicationResource` will be used as a base class for all serializers in the
  application - it includes the `Alba::Resource` module and instructs Alba to
  add the root key to the payload. Additionally, we enable the key
  transformation to convert attribute names to camel case.
- `GroupResource` defines the `id` and `name` attributes and renders the `items`
  association. Thanks to setting the inflector, we don’t need to manually
  specify the serializer class for the association.
- `ItemResource` renders the `content` attribute and the `is_completed` flag,
  for which we enable automatic type conversion.

And now, it’s time to put it all together in the controller. We will fetch the
records from the database and render them using the serializer:

```ruby
class ItemsController < ApplicationController
  def index
    groups = Group.includes(:items).all
    render json: GroupResource.new(groups)
  end
end
```

Start the server using `rage s` and navigate to
[http://localhost:3000/api/v1/items](http://localhost:3000/api/v1/items) to see
the rendered list.

## OpenAPI documentation

We’ve built our first endpoint, but how will the client application understand
how to use it and what data to expect? With Rage’s OpenAPI solution, building
the documentation comes down to two simple steps.

### Step 0

Mount the `Rage::OpenAPI` component at the URL of your choice:

```ruby
# config/routes.rb
mount Rage::OpenAPI.application, at: "/publicapi"
```

### Step 1

Document your endpoint:

```ruby
class ItemsController < ApplicationController
  # Get all Todo items grouped by their parent items.
  # @response Array<GroupResource>
  def index
    groups = Group.includes(:items)
    render json: GroupResource.new(groups)
  end
end
```

Here, we use YARD-style tags to specify what the endpoint does and what payload
it responds with using the Alba serializer. Without any additional annotations
or code changes, Rage will use the serializer to construct the response payload.
If you now visit
[http://localhost:3000/publicapi](http://localhost:3000/publicapi), you will see
the documentation with our list endpoint, its summary, and the response payload
built based on the Alba serializer:

![API docs](/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image-2.png)

_ℹ️ Use the [OpenAPI Explorer](https://openapi-explorer.rage-rb.dev) tool to
experiment with `Rage::OpenAPI` without creating a new project._

## Pagination

To allow clients to paginate through long lists of items using infinite scroll,
let’s also add simple offset-based pagination. To do that, we will need to
update our documentation to accept two optional parameters and update the Active
Record query:

```ruby
class ItemsController < ApplicationController
  # Get all Todo items grouped by their parent items.
  # @param limit? Number of items per page
  # @param offset? The offset of the first item to return
  # @response Array<GroupResource>
  def index
    limit  = params[:limit]  || 10
    offset = params[:offset] || 0
    groups = Group.includes(:items).limit(limit).offset(offset)

    render json: GroupResource.new(groups)
  end
end
```

## Create API

Let’s now add the create API. It will accept the details of a Todo item and
respond with the serialized record:

```ruby
# config/routes.rb
Rage.routes.draw do
  scope path: "api/v1" do
    resources :items, only: %i[index create]
  end
end

# app/controllers/items_controller.rb
class ItemsController < ApplicationController
  # Create a new Todo item.
  # @request { item: { content: String, group_id: Integer } }
  # @response ItemResource
  def create
    item = Item.create!(item_params)
    render json: ItemResource.new(item)
  end

  private

  def item_params
    params.fetch(:item).slice(:content, :group_id)
  end
end
```

In this code, we:

- Add the new action to the `resources` call in `config/routes.rb`.
- Define a new controller action and document it using OpenAPI tags. We use an
  inline YAML syntax to specify the request payload.
- Define the `item_params` method to filter the parameters required to create an
  item.
- Create a new item and render it back using the item serializer.

If you now visit
[http://localhost:3000/publicapi](http://localhost:3000/publicapi), you will see
the updated documentation:

![POST API docs](/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image-3.png)

## Real-time updates

We’ve built an API that allows us to view and create Todo items. But since our
Todo list is shared, we’d like users to be notified of each other’s actions. For
example, if Alice creates a new Todo item, it should automatically appear in
Bob’s application without him having to refresh the page. Meet `Rage::Cable`!

### Mount a new application

Let’s update the routes to mount the new component:

```ruby
# config/routes.rb
mount Rage::Cable.application, at: "/cable"
```

And create a new channel:

```ruby
# app/channels/items_channel.rb
class ItemsChannel < Rage::Cable::Channel
  def subscribed
    stream_from "todo-items"
  end
end
```

Here, we attach all subscribed clients to the `todo-items` stream. Clients will
be able to use the [actioncable](https://www.npmjs.com/package/actioncable)
package to subscribe to this stream and listen for updates.

Finally, let’s update our API to send notifications every time a new Todo item
is created:

```ruby
class ItemsController < ApplicationController
  # Create a new Todo item.
  # @request { item: { content: String, group_id: Integer } }
  # @response ItemResource
  def create
    item = Item.create!(item_params)
    render json: ItemResource.new(item)
    Rage::Cable.broadcast("todo-items", { action: "new-item", data: item })
  end
end
```

In this code, we’ve added the `Rage::Cable.broadcast` call to push the updates
to the clients. The first argument is the name of the stream to which the
clients are subscribed. The second argument is the payload, which can
essentially be any Ruby object - in this example, we specify that this message
represents a new item added to the list and send the actual item under the
`data` key.

## Why Choose Rage?

Hopefully, at this point you are thinking something along the lines of “Wait,
this is boring! It’s almost like Rails!”.

Yes! Rage is designed to
[feel familiar](https://mcfunley.com/choose-boring-technology), making it easy
for developers to transition and build robust applications quickly. It attempts
to work exactly the way you would expect it to. Still, while Rage’s syntax is
well-understood, behind the scenes it implements several major improvements
compared to existing frameworks.

Rage brings seamless asynchronous I/O and performance to Ruby, making Ruby a
more versatile technology. Rage
[delivers impressive performance](https://www.techempower.com/benchmarks) on par
with existing Python and Elixir solutions, and the support for asynchronous I/O
means you can now use Ruby to build services that would otherwise be built with
Node.js or Go.

### Benchmarks: Rage vs Ruby on Rails

**Hello World**

```ruby
class BenchmarksController < ApplicationController
  def index
    render json: { hello: "world" }
  end
end
```

![Hello world benchmark vs Ruby on Rails](/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image-4.png)

**Waiting on I/O**

```ruby
require "net/http"

class BenchmarksController < ApplicationController
  def index
    Net::HTTP.get(URI("<endpoint-that-responds-in-one-second>"))
    head :ok
  end
end
```

![Waiting on I/O benchmark vs Ruby on Rails](/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image-5.png)

**Using ActiveRecord**

```ruby
class BenchmarksController < ApplicationController
  def show
    render json: World.find(rand(1..10_000))
  end
end
```

![Using ActiveRecord benchmark vs Ruby on Rails](/media/posts/2025-04-13-ruby-rage-rest-api-tutorial/image-6.png)

## Wrapping Up

**Ready to give Rage a try?** The best way to understand Rage’s capabilities is
to dive in and experiment with building your own APIs. Check out Rage’s
[API docs](https://rage-rb.pages.dev/RageController/API) and
[Fiber helpers](https://rage-rb.pages.dev/Fiber) for a deeper understanding of
its features.

_ℹ️ Rage welcomes contributors! If you're interested in getting involved, check
out the repository for a list of
[good first issues](https://github.com/rage-rb/rage/issues)._

Zuplo has many features that will enhance your API and reduce the maintenance
burden on your development team. Give it a try!

To learn more, check out Zuplo's excellent
[documentation](https://zuplo.com/docs/articles/what-is-zuplo?utm_source=blog).