On Positivity and Rails API
I've been programming in Rails for years now, but have yet to make a publicly consumable API. Especially not one using the built in Rails API functionality. Today that ends. Read ahead as I document building a pretty simple API to be consumed by some projects I've had in mind.
Twitter has its place in the world, but truth be told I'm not a big Tweeter.
A few weeks ago, I looked into making a Twitter bot to disclose my thoughts on a regular basis. The bot would posts a tweet daily at 5pm if I hadn't already. Everybody would win. My followers would no longer be bored by my unending silence while I could go about my everyday life, not concerned with character limits.
I made the bot to read pre-saved tweets from a file, but the thought occurred to me: "What happens when that file runs out of tweets?" The file would require a repeated refreshing of tweets to keep up with the demand of time. Projecting my current rate of tweeting, 0 tweets/day, running out of tweets was guaranteed.
In lieu of a personalized message, I would like to be able to pull from a preconditioned list of things that I could put my personal brand behind. For me, personal growth and confidence is important, so I chose to have the backup tweets be filled with positive quotations.
Little did I know that the positive quotation API world is in shambles. The best one that exists is behind a paywall. The other feasible option required emailing the maintainer for a secret key and waiting a week to get it. (Go ahead. Google it yourself.)
So, that's the motivation for the following material. I went out into the Real World (gasp), and bought this book:
I'm going to spend a decent amount of time inputing chosen quotes into digital form, but as far as I'm concerned, that's time better spent than potentially scrolling through the never ending flame war that is Twitter.
The API. It's Rails.
If you're going to be following along, have the following on your system:
Ruby 2.3+ - Rails 5+ - Postgres 10+
I have a few requirements for this API. I want to be able to GET a specific quotation. I want to be able to easily PUT a new quotation (I'll be using POST, though.) I also want the ability to edit the quote if I notice I fat fingered the content or attribution. And as always, the code should be maintainable and extensible. But, that's it. No bulk GETs. No DELETEs.
Let's rev it up.
$ rails new --api --database=postgresql quotes
That makes a new rails API project in a directory called quotes. I chose Postgres because it's familiar; if you're following along, choose whatever DB you wish.
Let's make the main event: the quote
resource. The book that I will be using to source the quotes provides attribution to a person. I'll populate my quotes with the same information.
$ rails g model quote content:text attribution:string
I'll go in and add null: false
to both attribute fields in the migration file that was generated by this command (db/migrate/xxx_create_quotes.rb
), then run
$ rake db:create && rake db:migrate
Now that resource is storable in a database, let's set our model to reflect the database restrictions:
# app/models/quote.rb
class Quote < ApplicationRecord
validates :content, :attribution, presence: true
end
Nice.
Time set up how a user can access these quotes from the outside.
First, the routes:
# config/routes.rb
Rails.application.routes.draw do
scope :v1 do
resources :quotes, only: [:show, :create]
end
end
For most projects I make, I do an additional scoping to the :api
path. Because all foreseeable routes will be api routes, that convention seems a bit redundant here.
Running $ rake routes
generates the following:
Prefix Verb URI Pattern Controller#Action
quotes POST /api/v1/quotes(.:format) quotes#create
quote GET /api/v1/quotes/:id(.:format) quotes#show
Great. One route to make a new quote and one to retrieve a quote. Now to the controller to code up what happens when a user hits the routes. I want all of my controllers to handle JSON requests and their responses to be in JSON format. Rails API actually strips away the functionality to respond to JSON, so first I add the responders
gem and bundle it in.
# Gemfile
...
gem 'responders'
...
$ bundle install
Next, I make the file, app/controllers/quotes_controller.rb
. For now, I'm going to do the basic CRUD implementations of the create and show actions.
# app/controllers/quotes_controller.rb
class QuotesController < ApplicationController
respond_to :json
def show
quote = Quote.find_by(id: params[:id])
if quote
render json: quote
else
render json: { error: 'No quote found' }, status: 404
end
end
def create
quote = Quote.new(quote_params)
if quote.save
render json: quote
else
render json: { error: quote.errors }
end
end
private
def quote_params
params.require(:quote).permit(
:id,
:content,
:attribution
)
end
end
Start up the server to test out the actions: rails s
I hit the show error path, first: $ curl localhost:3000/v1/quotes/1
. This returns a 404 status code with a payload of { error: 'No quote found' }
.
Great.
Next the create error path. I use the Postman app for this. I send a POST request without a quote[content]
argument. This returns:
error: {
"content": [
"can't be blank"
]
}
Great. Now test the happy path; I provide all of the necessary attributes. This returns:
{
"id": 1,
"content": "test content",
"attribution": "test person",
"created_at": "2017-12-11T02:50:20.761Z",
"updated_at": "2017-12-11T02:50:20.761Z"
}
Great. As expected. Now let's try to GET the newly created quote; I curl localhost:3000/v1/quotes/1
and this is returned:
{
"id": 1,
"content": "test content",
"attribution": "test person",
"created_at": "2017-12-11T02:50:20.761Z",
"updated_at": "2017-12-11T02:50:20.761Z"
}
Great.
Next step is doing a bit of data modeling to ensure the code is maintainable.
To see this project in action, be sure to follow me - @pop_demand - on Twitter.
Stay tuned.
Edit: Next article is fresh off the presses! Check it out:
Written: January 19, 2018
Last updated: January 12, 2025