---
title: "How to build an API with Ruby and Sinatra"
description: "Learn how to build an API with Ruby and Sinatra. Implement API rate limiting, API key authentication and build an API developer documentation portal."
canonicalUrl: "https://zuplo.com/blog/2025/01/07/how-to-build-an-api-with-ruby-and-sinatra"
pageType: "blog"
date: "2025-01-07"
authors: "blag"
tags: "Ruby, API Tooling, Tutorial"
image: "https://cdn.zuplo.com/cdn-cgi/image/fit=crop,width=1200,height=630/www/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-40.png"
---
> This article is written by Alvaro, a member of the Zuplo community and
> longtime API builder. You can check out more of his work
> [here](https://medium.com/@atejada). All opinions expressed are his own.

Have you ever heard of Frank Herbert’s **Dune** saga? I’m not talking about the
movie—though it was pretty awesome. I’m talking about the books 🤓, which are on
a whole other level of amazing.

![Dune quotes](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-1.png)

If you’ve read the books (and if you haven’t, you absolutely should), you know
they’re packed with incredible quotes. Of course, it’s tough to remember them
all, so we’re going to create a small API project using Ruby and Sinatra to make
it easier. Later, we’ll use Zuplo to take our API to the next level of
awesomeness 😎.

## What are we going to do today?

1. [Creating the project](#creating-the-project)
2. [Adding the required gems](#adding-the-required-gems)
3. [Creating a MongoDB Atlas Account](#creating-a-mongodb-atlas-account)
4. [Installing MongoDB Shell and creating our quotes database](#installing-mongodb-shell-and-creating-our-quotes-database)
5. [Populating our quotes collection](#populating-our-quotes-collection)
6. [Testing our API](#testing-our-api)
7. [Hosting our API for the world to see](#hosting-our-api-for-the-world-to-see)
8. [Creating a project on Zuplo](#creating-a-project-on-zuplo)
9. [Adding a Rate Limit](#adding-a-rate-limit)
10. [Setting API Key Authentication](#setting-api-key-authentication)
11. [Developer Documentation Portal](#developer-documentation-portal)

## Creating the project

First, create a folder named **duneQuotes** and add a file called **server.rb**
inside it. Since this is a straightforward project, we’ll keep all our source
code in a single file for simplicity.

```shell
$ mkdir duneQuotes && cd duneQuotes
$ nano server.rb
```

## Adding the required gems

For this project, we’ll use Sinatra, as mentioned earlier, along with
[MongoDB Atlas](https://www.mongodb.com/lp/cloud/atlas/try4-reg?adgroup=152623366744&cq_cmp=19647113651)
and a few additional gems. To get started, create a file named **Gemfile** and
add the following:

```ruby
# Gemfile source 'https://rubygems.org'
gem 'sinatra'
gem 'mongoid'
gem 'sinatra-contrib'
gem 'rackup'
gem 'puma'
gem 'ostruct'
gem 'json'
```

To install all the dependencies, run the following command:

```shell
bundle install
```

## Creating a MongoDB Atlas account

Go here and create your
[free account](https://account.mongodb.com/account/register). It’s
straightforward but here’s a little
[guide](https://www.mongodb.com/docs/guides/atlas/account/) just in case.

In the end, you’re going to receive a string like this one:

```shell
mongodb+srv://<USER>:<PASSWORD>@blagcluster.<URL>.mongodb.net/quotes?retryWrites=true&w=majority&appName=BlagCluster
```

## Installing MongoDB Shell and creating our quotes database

We’ll use [Homebrew](https://brew.sh/) (Brew) for the installation process.:

```shell
$ brew install mongosh
```

After installation, create a file named **mongoid.yml** and add the following
content::

```yaml
development:
  clients:
    default:
      uri: "mongodb+srv://<USER>:<PASSWORD>@blagcluster.<URL>.mongodb.net/quotes?retryWrites=true&w=majority&appName=BlagCluster"
      options:
        auth_mech: :scram
        server_selection_timeout: 5
    options: log_level: :warn
```

This will help us establish the connection with MongoDB Atlas.

This will create the database, and in the **server.rb** file, we’ll define the
collection (or document):

```ruby
# server.rb
require 'sinatra'
require "sinatra/namespace"
require 'mongoid'
require 'json'

Mongoid.load! "mongoid.config"

class Quote
  include Mongoid::Document

  field :quote, type: String
  field :character, type: String

  validates :quote, presence: true
  validates :character, presence: true

  index({ character: 'text' })

  scope :character, -> (character, limit = nil) {
  query = where(character: /^#{character}/)
  limit ? query.limit(limit) : query
  }

end

get '/' do
  '🐭🌖 Dune Quotes 🐭🌖'
end

namespace '/api/v1' do
  before do
    content_type 'application/json'
  end

  get '/quotes' do
    quotes = Quote.all

    quotes.to_json
  end

end
```

Before running this (though it will be empty since we haven’t populated our
collection yet), let’s take a moment to analyze the code:

```ruby
Mongoid.load!("mongoid.yml", :development)
```

We’re loading our MongoDB configuration file:

```ruby
class Quote
  include Mongoid::Document

  field :quote, type: String
  field :character, type: String

  validates :quote, presence: true
  validates :character, presence: true

  index({ character: 'text' })

  scope :character, -> (character, limit = nil) {
  query = where(character: /^#{character}/)
  limit ? query.limit(limit) : query
  }
end
```

We’re defining our collection. We’re going to have two fields, quote and
character. Both need to be present at the time of data insertion. We’re going to
index our collection by character. The last part means that we want to use
Regular Expressions to find a character’s quote without needing to specify its
full name, also, it means that we can specify how many records we want to get
back.

```ruby
get '/' do
  '🐭🌖 Dune Quotes 🐭🌖'
end
```

This is what we’ll see when we call the main API.

```ruby
namespace '/api/v1' do
  before do
    content_type 'application/json'
  end

  get '/quotes' do
    quotes = Quote.all

    quotes.to_json
  end
```

We’ll create a namespace to enable versioning for our API, and ensure that the
responses are formatted as JSON.

Our first endpoint will fetch and return all quotes.

## Populating our quotes collection

Let’s create a YAML file named **Quotes.yaml** with the following content::

```yaml
- quote: "He who can destroy a thing, controls a thing."
  character: "Paul Atreides"
- quote: "All paths lead into darkness."
  character: "Paul Atreides"
- quote: "The eye that looks ahead to the safe course is closed forever."
  character: "Paul Atreides"
- quote:
    "Motivating people, forcing them to your will, gives you a cynical attitude
    towards humanity. It degrades everything it touches."
  character: "Lady Jessica"
- quote:
    "When we try to conceal our innermost drives, the entire being screams
    betrayal."
  character: "Lady Jessica"
- quote:
    "Once you have explored a fear, it becomes less terrifying. Part of courage
    comes from extending our knowledge."
  character: "Duke Leto Atreides"
- quote:
    "Respect for the truth is the basis for all morality. Something cannot
    emerge from nothing."
  character: "Duke Leto Atreides"
- quote: "The slow blade penetrates the shield."
  character: "Gurney Halleck"
- quote:
    "I must not let my passion interfere with my reason. That is not good. That
    is bad."
  character: "Piter De Vries"
- quote: "I must not fear. Fear is the mind-killer."
  character: "Bene Gesserit"
- quote: "There is no escape—we pay for the violence of our ancestors."
  character: "Paul Muad’Dib"
```

Next, we’ll create a script to load our quotes:

```ruby
require './server.rb'
require 'yaml'

quotes = YAML.load_file("Quotes.yaml")
quotes.each do |quote_data|
  begin
    quote = Quote.new(quote: quote_data["quote"], character: quote_data["character"])
    quote.save
    puts "Document inserted successfully."
  rescue Mongo::Error::OperationFailure => e
    puts "Insertion failed: #{e.message}"
  end
end

```

This will load our **server.rb** file, load the YAML file, and then iterate
through each entry to perform an insert using the **quote** and **character**.

You can run it by typing::

```shell
$ ruby Load_Quotes.rb
```

![inserting quotes](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-2.png)

We can verify it by loading our collection in the MongoDB shell:

```shell
$ mongosh "mongodb+srv://blagcluster.<URL>.mongodb.net/" --apiVersion 1 --username <USER> --quiet
```

![verifying quote insertion](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-3.png)

Once we’re logged in, we can check our collection by running::

```ruby
$ use quotes $ db.quotes.find()
```

![quote collection](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-4.png)

<a id="testing-our-api"></a>

## Testing our API

Now that we have data to work with, let’s start our server and test the API.

```shell
bundle exec ruby server.rb
```

Open your favorite web browser and navigate to
**http://localhost:4567/api/v1/quotes**:

![Ruby API response](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-5.png)

Success! It’s working as expected, but it’s a bit too simple. Let’s add a few
more endpoints to make it more useful and feature-rich:

```ruby
namespace '/api/v1' do
  before do
    content_type 'application/json'
  end

  get '/quotes' do
    quotes = Quote.all

    [:quote, :character].each do |filter|
      quotes = quotes.send(filter, params[filter]) if params[filter]
    end

   if params[:limit]
     limit = params[:limit].to_i
     quotes = quotes.limit(limit)
   end

    quotes.to_json
  end

  get '/quotes/id/:id' do |id|
    quote = Quote.where(id: id).first
    halt(404, { message:'Quote Not Found on Arrakis'}.to_json) unless quote
    quote.to_json
  end

  get '/quotes/random' do
    quote = Quote.collection().aggregate([{ '$sample' => { 'size' => 1 } }])
    quote.to_json
  end

end
```

Let’s review the code before running it:

```ruby
get '/quotes' do
    quotes = Quote.all

    [:quote, :character].each do |filter|
      quotes = quotes.send(filter, params[filter]) if params[filter]
    end

   if params[:limit]
     limit = params[:limit].to_i
     quotes = quotes.limit(limit)
   end

    quotes.to_json
  end
```

We’re adding the ability to filter quotes by character. For example, to retrieve
all quotes by Paul Atreides, you can enter **Pau**, **Paul**, or **Paul A**.
Additionally, we’re introducing a limit option, allowing you to specify how many
records to retrieve, such as **1**, **2**, or more.

```ruby
  get '/quotes/id/:id' do |id|
    quote = Quote.where(id: id).first
    halt(404, { message:'Quote Not Found on Arrakis'}.to_json) unless quote
    quote.to_json
  end
```

If we know the ID, we can use it to select a specific quote. If an incorrect ID
is provided, the quote won’t be found—just like water on Arrakis 🤓:

```ruby
  get '/quotes/random' do
    quote = Quote.collection().aggregate([{ '$sample' => { 'size' => 1 } }])
    quote.to_json
  end
```

Sometimes, we might just want to fetch a random quote—which makes perfect sense,
as always selecting the same one would be quite dull.

Alright, let’s test the new functionalities:

![Get character response](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-6.png)

![Get quote response](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-7.png)

![Limiting API results](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-8.png)

Excellent! It works as expected, but only on our local machine. Wouldn’t it be
amazing to share it with the world?

First, we need to upload our project to GitHub. Keeping it private won’t cause
any issues.

## Hosting our API for the world to see

When it comes to hosting an API, there are plenty of options available. For this
particular API, we’ll use [Render](https://render.com/).

We need to create a new web service and select the repository we want to use—in
this case, **duneQuotes**.

![Render web service creation](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-9.png)

![Connecting to a git repo](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-10.png)

Don’t forget to update the start command to correctly call our **server.rb**
file:

![Setting the state command](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-11.png)

It’s crucial to click on **Connect** and copy the three static IP addresses used
by Render:

![Copying IP addresses](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-12.png)

We’ll use those three IPs in MongoDB Atlas to allow access under **Network
Access**; otherwise, the connection will be denied:

![Network access to MongoDB Atlas](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-13.png)

Once Render finishes deploying our API, we’ll be able to access the main entry
point using:

```http
https://dunequotes.onrender.com
```

We can then call an endpoint using:

```http
https://dunequotes.onrender.com/api/v1/quotes?character=Pau&limit=2
```

![API response](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-8.png)

Are we done yet? Yes and no. If we’re happy with it as is—simple, unsecured,
and, well, a bit amateurish—then we’re good. But if we want to make it cooler
without too much effort, we can start using Zuplo and level up the experience.

![Zuplo logo](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-14.png)

## Creating a project on Zuplo

After creating a
[Zuplo account](https://portal.zuplo.com/signup?utm_source=blog), we’ll need to
set up a new project. We’ll name it **dune-quotes**, select an empty project,
and then create it. Wondering why Zuplo? Imagine having to build rate limits,
authentication, monetization, and other features entirely on your own. That’s a
lot of work and hassle. Zuplo simplifies all of that, making it a breeze.

![Creating a Zuplo project](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-15.png)

Once the project is created, we’ll be greeted with this screen. From here,
simply press **Start Building**:

![Start building a Zup](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-16.png)

To start enhancing our API, click on **routes.oas.json**:

![Navigating to routes.oas.json](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-17.png)

Next, we need to add a route, which Zuplo will manage:

![Adding a route](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-18.png)

The most important fields here are **Path** and **Forward to**:

![Configuring the path](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-19.png)

After configuring the route, we need to save it. The save button is located at
the bottom, or you can press **[Command + S]** on a Mac.

![Saving your changes](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-20.png)

The build process is blazing fast. Clicking the **Test** button allows us to, no
pun intended, test our API integration.

![Testing the API](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-21.png)

Success! 🥳🥳🥳 Our Zuplo integration is working perfectly!

## Adding a Rate Limit

Most likely, we don’t want people abusing our API or attempting to take down our
server with DDoS attacks. While coding a rate limit policy isn’t difficult,
there are many factors to consider—so why bother? Let’s have Zuplo manage that
for us.

![Adding a policy](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-23.png)

We need to add a new policy on the request side. Since we’ll be dealing with
many policies, simply type **rate** and select **Rate Limiting**:

![Selecting rate limiting](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-24.png)

All we need to do here is press **Ok**. Easy, right?

![Adding rate limiting](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-25.png)

Our rate limit has been added. Now, we just need to save and run the test three
times. On the third attempt, we’ll hit the rate limit. Of course, we can adjust
this in the policy, as shown in the image above, where **requestsAllowed** is
set to 2.

![Saving rate limiting](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-26.png)

Exceeding the request limit will temporarily block further data requests. We’ll
need to wait a minute before trying again.

![Testing rate limiting](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-27.png)

So far, so good—but what if we want to prevent unauthorized access to our API?
We’ll need to implement some sort of authentication. While it’s not overly
difficult to build, it involves multiple steps, configurations, and testing.
Wouldn’t it be great to let Zuplo handle those details for us? That way, we can
focus solely on our API.

## Setting API Key Authentication

We need to navigate back to the **Policies** section and add a new policy—this
time, **API Key Authentication**:

![Selecting API key authentication](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-28.png)

There’s not much to do here—just press **OK**.

![Configuring API key authentication](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-29.png)

It’s important to move the **api-key-inbound** policy to the top:

![Rearranging policies](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-30.png)

If we save it and try testing the API, access will be denied:

![Testing API key authentication rejection](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-31.png)

At the top of the screen, click **Services**, then select **Configure** under
**API Key Service**:

![Navigating the services tab](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-32.png)

We need to create a consumer, which will function as a token:

![Creating a consumer](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-33.png)

We need to set a name for the subject and specify the email or emails associated
with the key:

![Adding a consumer](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-34.png)

Once we click **Save consumer**, an API key will be generated. We can either
view it or copy it:

![Saving the API consumer](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-35.png)

Now, we can go back to **Code** → **routes.oas.json** to test our API. Here, we
need to add the authorization header and pass the **Bearer** token along with
the API key:

![Testing the API key](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-22.png)

Success once again! It’s working as expected! 🥳🥳🥳

If you think that doing this manually is not for you, read this:

![API key service API](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-36.png)

And that’s how you enhance your API with Zuplo 😎 But wait, there’s more! As
always, we have a cherry on top.

## Developer Documentation Portal

If we click on **Gateway** at the bottom of the screen, we’ll see a link to the
Developer Portal:

![API developer portal link](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-37.png)

Here, we can see that every route we create will be added to the Developer
Portal:

![API developer portal](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-38.png)

If we log in, we can see our API keys:

![Developer portal post-login](/media/posts/2025-01-07-how-to-build-an-api-with-ruby-and-sinatra/image-39.png)

So, what are you waiting for? Give Zuplo a try! There are tons of features that
will make your API stand out, and your development team will be happy they won’t
have to maintain everything on their own.

If you’d like to check out the source code for the API, here’s a clean version
without the MongoDB Atlas keys,
[duneQuotesPublic](https://github.com/atejada/duneQuotesPublic).

If you want to learn more about Zuplo, check out their
[documentation](https://zuplo.com/docs/articles/what-is-zuplo?utm_source=blog)—it’s
a fantastic resource.

Now, what can you build with Zuplo? 😎