How to build an API with Ruby and Sinatra

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. 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

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
  2. Adding the required gems
  3. Creating a MongoDB Atlas Account
  4. Installing MongoDB Shell and creating our quotes database
  5. Populating our quotes collection
  6. Testing our API
  7. Hosting our API for the world to see
  8. Creating a project on Zuplo
  9. Adding a Rate Limit
  10. Setting API Key Authentication
  11. 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.

$ 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 and a few additional gems. To get started, create a file named Gemfile and add the following:

# 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:

bundle install

Creating a MongoDB Atlas account#

Go here and create your free account. It’s straightforward but here’s a little guide just in case.

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

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 (Brew) for the installation process.:

$ brew install mongosh

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

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):

# 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:

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

We’re loading our MongoDB configuration file:

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.

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

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

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::

- 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:

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::

$ ruby Load_Quotes.rb

inserting quotes

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

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

verifying quote insertion

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

$ use quotes $ db.quotes.find()

quote collection

Testing our API#

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

bundle exec ruby server.rb

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

Ruby API response

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:

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:

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.

  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 🤓:

  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

Get quote response

Limiting API results

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.

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

Render web service creation

Connecting to a git repo

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

Setting the state command

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

Copying IP addresses

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

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

https://dunequotes.onrender.com

We can then call an endpoint using:

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

API response

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

Creating a project on Zuplo#

After creating a Zuplo account, 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

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

Start building a Zup

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

Navigating to routes.oas.json

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

Adding a route

The most important fields here are Path and Forward to:

Configuring the path

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

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

Testing the API

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

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

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

Adding rate limiting

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

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

Testing rate limiting

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

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

Configuring API key authentication

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

Rearranging policies

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

Testing API key authentication rejection

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

Navigating the services tab

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

Creating a consumer

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

Adding a consumer

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

Saving the API consumer

Now, we can go back to Coderoutes.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

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

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

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

API developer portal

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

Developer portal post-login

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.

If you want to learn more about Zuplo, check out their documentation—it’s a fantastic resource.

Now, what can you build with Zuplo? 😎

Questions? Let's chatOPEN DISCORD
0members online

Designed for Developers, Made for the Edge