APIs are essential for the internet. They allow applications to interact programmatically, making developers’ lives easier. However, web applications are often targeted by malicious actors, which is why rate limiting is crucial for API security. In this guide, we’ll use Ruby on Rails to create an API application for managing an animal shelter and integrate rate limiting with rack-attack. We’ll track cats, dogs, and volunteers through a JSON interface.
You can follow along with the example app we create and view the final app’s source code on Github. Let’s dive in!
Installing Ruby
If you don’t have Ruby installed, you’ll need to do so. I’ll be using Ruby 3.2 in this tutorial. It’s recommended to use a Ruby version manager like rbenv if you’re starting fresh. You can install rbenv with Homebrew:
Next, install Ruby 3.2.0 using rbenv:
Then, set rbenv to use this Ruby version for your current directory:
Installing Rails
Now that you have Ruby installed, it’s time to install Rails. We’ll be using Rails 7.1.1 in this tutorial. You can install it by running:
Generating a new Rails project
Rails is a full-stack framework, but you can generate an API-only project if you only need to serve JSON data. This is useful for integration with mobile apps, SPAs, or other services. Create a new Rails 7.1.1 API project with:
Then, navigate to the directory of the new application, in this case, animal-shelter.
For this example, we’ll focus on improving the operations of an animal shelter. We’ll create API endpoints for cats, dogs, and volunteers. While this may not be practical on its own, it sets the foundation for more advanced features. If you want to name your application differently, replace “animal-shelter” in the command above.
Using Rails scaffolds for tracking cats
To create, read, update, and delete cats, we’ll use Rails scaffolding. This will generate models, controllers, and a migration file. Run the following command in your application:
This command generates various files necessary for managing cats.
Run the pending database migration created by the scaffold:
Inspecting the cats code
In the app/controllers/cats_controller.rb file, you’ll find prepopulated methods that enable HTTP requests for creating, reading, updating, and deleting cats. Here’s the cats controller:
before_action :set_cat, only: %i[ show update destroy ]
# GET /cats
def index
@cats = Cat.all
render json: @cats
end
# GET /cats/1
def show
render json: @cat
end
# POST /cats
def create
@cat = Cat.new(cat_params)
if @cat.save
render json: @cat, status: :created, location: @cat
else
render json: @cat.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /cats/1
def update
if @cat.update(cat_params)
render json: @cat
else
render json: @cat.errors, status: :unprocessable_entity
end
end
# DELETE /cats/1
def destroy
@cat.destroy!
end
private
# Use callbacks to share common setup or constraints between actions.
def set_cat
@cat = Cat.find(params[:id])
end
# Only allow a list of trusted parameters through.
def cat_params
params.require(:cat).permit(:name, :arrival_date, :description)
end
end
Testing the cats code
You can test the functionality by using curl or a tool like Postman. Send a POST request to localhost:3000/cats with the specified body to create a cat in the database:
“name”: “Bear”,
“description”: “Bear is aloof but loving. She prefers not to be picked up, but in her own time will join happily you for a cuddle.”,
“arrival_date”: “2024-01-28 14:11:09.818508”
}
This will create a new cat in the database and return a 201 HTTP status with the created object in the response:
“id”: 1,
“name”: “Bear”,
“arrival_date”: “2024-01-28T14:11:09.818Z”,
“description”: “Bear is aloof but loving. She prefers not to be picked up, but in her own time will join happily you for a cuddle.”
}
“created_at”: “2024-01-28T19:13:51.047Z”,
“updated_at”: “2024-01-28T19:13:51.047Z”
}
Manually building endpoints for tracking dogs
Using Rails’ generators is incredibly convenient, and you have the opportunity to edit the generated files. Still, it’s not great for learning purposes. We’ll build a set of endpoints for CRUD operations on a ‘dog’ resource next, this time without the generators.
Creating a model
First, we’ll create the model file. In app/models, create a new file called dog.rb. The file doesn’t need a lot, just a class name and the class it should inherit from:
end
Inheriting from ApplicationRecord tells Rails that this is a model and provides the class with a set of methods that it will need. Next, we’ll create the necessary table in the database to persist records of dogs. We’ll first create a new migration by running:
Creating a new database table
This creates a new timestamped file in the db/migrations directory. In that file, you’ll see a change method, which will be executed when the migration is run. The change method already creates a new table with the right name but is missing the rows we want. Change the migration to this:
def change
create_table :dogs do |t|
t.string :name
t.datetime :arrival_date
t.text :description
t.timestamps
end
end
end
Next, run the migration with:
Creating routes for the dog resource
Next, we must create routes telling the application where to send incoming web requests. These are defined in config/routes.rb with a DSL that makes this convenient. Inside the code block (before the final end), add this line:
Writing the DogsController
Finally, we’ll write out the DogsController, which will handle CRUD actions for the dog resource. Create a new file in app/controllers/ called dogs_controller.rb There are a few different ways to write the code as long as you contain write the appropriate public methods, but you can speed things up by copying the CatsController and replacing every cat with dog:
before_action :set_dog, only: %i[ show update destroy ]
# GET /dogs
def index
@dogs = Dog.all
render json: @dogs
end
# GET /dogs/1
def show
render json: @dog
end
# POST /dogs
def create
@dog = Dog.new(dog_params)
if @dog.save
render json: @dog, status: :created, location: @dog
else
render json: @dog.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /dogs/1
def update
if @dog.update(dog_params)
render json: @dog
else
render json: @dog.errors, status: :unprocessable_entity
end
end
# DELETE /dogs/1
def destroy
@dog.destroy!
end
private
# Use callbacks to share common setup or constraints between actions.
def set_dog
@dog = Dog.find(params[:id])
end
# Only allow a list of trusted parameters through.
def dog_params
params.require(:dog).permit(:name, :arrival_date, :description)
end
end
Testing the DogsController
Finally, you can create, read, update, and delete dogs via the API just like you can for cats! Restart your rails server with rails s, then you can use Postman to create a new dog like this:
A screenshot of creating a dog with the API via Postman
Using scaffolds to build endpoints for tracking volunteers
Lastly, we’ll use the scaffold again to create endpoints for tracking volunteers. To start, run:
Next, run the newly generated migration with:
This gives us everything we need for volunteers!
Integrating rack-attack for rate limiting
Now that we have a working Rails API with three different resources, we have a functional application for which we can add rate limiting! Rate limiting helps protect your application against malicious actors. Put simply, it throttles the amount of requests that someone can make to the API in a given time period. This adds a layer of protection to mitigate scraping, DoS, DDoS, and brute force attacks.
We can use a popular Ruby Gem called rack-attack to add rate limiting to this API. First, add the gem to the Gemfile:
Next, install it with:
Next, we’ll configure rack-attack. Create a new file in config/initializers called rack-attack.rb.
In that configuration, we’ll add the setup from rack’s documentation. We’ll strip out all of the configuration except that which throttles requests by IP.
Your config should look like this:
### Configure Cache ###
# If you don’t want to use Rails.cache (Rack::Attack’s default), then
# configure it here.
#
# Note: The store is only used for throttling (not blocklisting and
# safelisting). It must implement .increment and .write like
# ActiveSupport::Cache::Store
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
### Throttle Spammy Clients ###
# If any single client IP is making tons of requests, then they’re
# probably malicious or a poorly-configured scraper. Either way, they
# don’t deserve to hog all of the app server’s CPU. Cut them off!
#
# Note: If you’re serving assets through rack, those requests may be
# counted by rack-attack, and this throttle may be activated too
# quickly. If so, enable the condition to exclude them from tracking.
# Throttle all requests by IP (60rpm)
#
# Key: “rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}”
throttle(‘req/ip’, limit: 300, period: 5.minutes) do |req|
req.ip # unless req.path.start_with?(‘/assets’)
end
### Custom Throttle Response ###
# By default, Rack::Attack returns an HTTP 429 for throttled responses,
# which is just fine.
#
# If you want to return 503 so the attacker might be fooled into
# believing that they’ve successfully broken your app (or you just want to
# customize the response), then uncomment these lines.
# self.throttled_responder = lambda do |env|
# [ 503, # status
# {}, # headers
# [”]] # body
# end
end
This contains helpful comments and sets a throttle for the entire application. If the application receives more than 300 requests in a 5-minute period, it will return a 429.
Testing our rate limiting
We can test our rate limiting in a number of ways, so let’s first create a few more cats in the database with a few POST /cats to our application. Once we’ve done that, we can GET /cats and see an array of existing cats in our database.
A screenshot of Postman getting an index of all cats
Next, we can temporarily change our configuration in rack-attack.rb to allow even fewer requests per 5-minute period.
#
# Key: “rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}”
throttle(‘req/ip’, limit: 5, period: 5.minutes) do |req|
req.ip # unless req.path.start_with?(‘/assets’)
end
Also, we’ll need to add in a chunk of code that configures rack-attack to use the MemoryStore cache if Redis is not present. Without this, rack-attack will not work in development, as the default cache in development is :null_store. In rack-attack.rb, add:
cache.store = ActiveSupport::Cache::MemoryStore.new
end
Without the placeholder comments, rack-attack.rb now looks like:
if !ENV[‘REDIS_URL’] || Rails.env.test?
cache.store = ActiveSupport::Cache::MemoryStore.new
end
# Throttle all requests by IP (60rpm)
# Key: “rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}”
throttle(‘req/ip’, limit: 5, period: 5.minutes) do |req|
req.ip # unless req.path.start_with?(‘/assets’)
end
end
Lastly, you can use Postman to make six subsequent requests to the application on any endpoint. Six requests to GET /cats now returns a 429!
A screenshot of Postman getting rate limited
This application-wide rate limiting is a great catch-all, but rack-attack allows you to do much more! You can limit specific endpoints, limit the total number of requests (disregarding source IP), blocklist, safelist, and more!
Conclusion
This tutorial has provided a comprehensive guide to building a Ruby on Rails API application with rate limiting to protect it! We’ve explored using the scaffold to quickly generate endpoints and also creating them manually. Rate limiting using rack-attack is a very helpful feature, safeguarding our application against abuse. With these tools and techniques at your disposal, you’re now equipped to create robust and efficient Rails API applications tailored to your specific needs. I encourage you to explore the rack-attack documentation to learn more about rate limiting at a more granular level than we explored.
It is important to always wear a helmet while riding a bicycle.
Make sure to properly adjust the straps and ensure a snug fit to protect your head in case of a fall.
Stay safe and happy riding!
HTML tags allow you to format and structure your content on the web. They provide a way to create headings, paragraphs, lists, links, images, and more. By using HTML tags, you can control the appearance and layout of your web pages, making them more visually appealing and easier to read.