In the final stage of my coding bootcamp, when I had become familiar with Ruby on Rails, one project I wanted to do was to write my own API.
So if you are viewing a web application that requires data from an API, your browser will send the request to the API, get the requested data back, and then render that data into a format that is suitable for you, a human, to view.
Hence, instead of HTML and CSS, APIs send data in more machine-friendly formats such as JSON, YAML, or XML.
For my API, I wanted to do something fun. My coding class had previously learned how to use Faker to seed our databases with mock information, and I recalled that one of Faker's libraries consisted of Chuck Norris jokes. I decided to go with this and create an API that sends out Chuck Norris jokes.
Let's build the API! First I create the directory and go into it:
$ mkdir chuck-norris-api $ cd chuck-norris-api
Once in the directory, I instruct Rails to generate the app:
$ rails new chuck-norris-api --api --database=postgresql
--api option (which is available in Rails 6) tells Rails that you don't want it to generate a full app -- only enough for an API to run. I set the second option
--database=postgresql as I will be using Heroku to host the API, and Heroku does not support Rails' default SQLite database.
Once the app is generated, I open my code editor and edit the
Gemfile located at the root of the directory. I add
gem 'faker' and uncomment
As I explained earlier, I will be using Faker to populate my database with Chuck Norris jokes, while Rack CORS is the middleware I need to ensure that the front-end I will be building to display the jokes will be able to receive the jokes from the API (more on that later).
I then run
bundle install in the terminal to install the new gems. Once that's done, it's time to build out the app's MVC!
First, the models. In this case, there's only one: the jokes. Each joke has one field: its content. In the terminal I instruct Rails to generate the model:
$ rails g model Joke content
This create the following migration:
In the terminal I instruct Rails to run the migration:
$ rails db:migrate
This creates the Jokes table in the database:
The database can now be populated with the Chuck Norris jokes. In
seeds.rb I instruct Rails to retrieve a hundred jokes from Faker's Chuck Norris library:
In the terminal, I then instruct Rails to run
seeds.rb and populate the database:
$ rails db:seed
Now that the model and database are sorted, I can proceed with the controller and view (the remaining two-thirds of the app's MVC). In the
app\controllers folder I create the nested folders
api\v1. In the
v1 folder I create the
In my controller, I only allow the user to perform two actions: get all jokes, and get one particular joke. I do not want the user to edit or delete existing jokes or to add new jokes. This is for the simple reason that I do not want to have to keep checking the database for inappropriate content once the API is deployed. Hence I do not include these remaining CRUD operations in the controller.
As with the controller, I add nested folders (
api\v1\jokes) in the
app\views folder. The
api\v1\jokes folder has two files corresponding to the two controller actions. The first is the
index.json.jbuilder file which will build and return a JSON file containing an array of all one hundred Chuck Norris jokes when a request for all jokes is received. In particular, I instruct the JSON builder to only include the content of each joke in the array, and leave out the other fields such as the id numbers of the jokes:
The second is the
show.json.jbuilder file which will return the joke with the particular id sent by the controller. As with the index file, I instruct the JSON builder to only include the content of the joke in the JSON file that is sent back to the user:
Now that the MVC are done, we can now proceed to the routes. In the
config\routes.rb file I specify the nested
v1 namespaces that correspond to the nested
api\v1 folders where the controller is housed, and I also specify that the only actions offered by the API are the
index (get all jokes) and
show (get one joke) actions, rather than the full suite of CRUD operations:
There is one final step that I need to do before I can deploy the app to production. It is a security feature of modern web browsers that, by default, they do not retrieve data from APIs if the domain of the requesting web page is different from the domain of the API. This will only be allowed if the API specifically allows web pages hosted on that particular domain to retrieve its data.
The obvious way to get around this restriction is to host the web page on the same domain as the API. But what is the fun in that? For this project, the API will be hosted on Heroku, and the web page which I will be displaying the jokes will be hosted on GitHub Pages. This is where the Rack CORS gem that had been installed earlier comes in. In the
config\initializers\cors.rb file I specify that my GitHub Pages domain is to be allowed:
While it is certainly possible to set
origins '*' in
cors.rb and allow any website to access the API, this will effectively bypass the security protocol. So I will be following the best practice and curate which domains are allowed to use the API. It should be noted that this is a security feature of web browers. As we shall see, if you use a tool like curl or Postman to access the API, this cross-domain security block will not apply.
The app is now ready to be deployed! It can be accessed at
https://rails-chuck-norris-api.herokuapp.com and as noted, you can use
curl from your terminal to retrieve all the jokes:
curl -s https://rails-chuck-norris-api.herokuapp.com/api/v1/jokes | jq
... or any one of the one hundred jokes. Here
curl retrieves joke #99:
curl -s https://rails-chuck-norris-api.herokuapp.com/api/v1/jokes/99 | jq
Chuck Norris has the final word:
I found this to be a really fun project to do, and I hope it will inspire you to create your own API!