How I Have Created an Eventbrite Clone with Ruby on Rails: Part VI

Brenda Zhang
14 min readFeb 8, 2022

How to add REST APIs to existing Ruby on Rails project with devise

Photo by Fotis Fotopoulos on Unsplash

Part I Part II Part III Part IV Part V

This is part 6 of my tutorial on how to complete a project of cloning Eventbrite with Ruby on Rails from scratch (full project description here), which is a part of the Ruby on Rails Full Stack Path (link here) from the Odin Project. I have created REST API endpoints and non-REST API endpoints for my application. This part is not in the requirements from theOdinProject, but I have done it as a way of learning and improving my technical skills.

In this part, I will mainly show you how I added REST APIs to my existing Ruby on Rails project which was implemented using devise gem (which made some difference about how to create the REST APIs).

To figure out how to properly add a private API key for the User model, I referred to this article.

Step 1. Add Encrypted Private API Key Column to Users Model

First, we’ll add two gems in the Gemfile. Lockbox is used as a means to encrypt the private API key for each user, and even if the database were compromised, the keys would still not be exposed. We add blind_index to query against the key and to ensure the key is unique. For more details about the two gems, refer here and here.

gem 'lockbox'
gem 'blind_index'

Then run ‘bundle install’ in the terminal to install the two gems.

Open Rails Console and configure Lockbox, like in Fig 1:

rails c
Lockbox.generate_key
Fig 1. Generate key for Lockbox in Rails Console

Copy the key and then open config/application.yml file, paste the saved key under LOCKBOX_MASTER_KEY, as in Fig 2:

Fig 2. Paste the saved key under LOCKBOX_MASTER_KEY in config/application.yml

Now exit your Rails Console and enter it again to see if you can see the value of LOCKBOX_MASTER_KEY by typing “ENV” in your Rails Console. If you can see it, it means that the value is added in your env.

Add a new file lockbox.rb in config/initializers, and add the following line:

Lockbox.master_key = ENV["LOCKBOX_MASTER_KEY"]

Create a migration to add the private API key to the User model.

rails g migration add_private_api_key_ciphertext_to_users private_api_key_ciphertext: text
Fig 3. Run rails migration to add the private api key to the User model

Then add the code in Fig 4 to the generated migration file in db/migrate:

Fig 4. Add the code to the generated migration file in db/migrate

Now run rails db:migrate in the terminal, as in Fig 5.

Fig 5. Run rails db:migrate in the terminal

With the database migration done, we can set the private api key in the User model via a callback. Add the code in Fig 6 to /app/models/user.rb:

Fig 6. Add the code to /app/models/user.rb

What the code in Fig 6 is doing is to to encrytp private_api_key and ensure the private_api_key is unique through the validation, then we use the callback function set_private_api_key to generate a unique value from SecureRandom.hex for each user if the private_api_key is nil.

Step 2. Allow Users to View and Rotate Private API Key

First, let’s generate a controller for the private API keys. In the terminal, run the following command:

rails g controller users/private_api_keys

Then add the following code in Fig 7 to app/controllers/users/private_api_keys_controller.rb:

Fig 7. Add the code in app/controllers/users/private_api_keys_controller.rb

Next we’ll add the view to display and update the private API key in app/views/devise/registrations/edit.html.erb. Add the code in the highlighted part in Fig 8 below the Update button.

Fig 8. Code to add in app/views/devise/registrations/edit.html.erb

Refresh your localhost:3000, and if there is an error, try and see if restarting the Rails server will fix the error. If everything works, in your User Edit page, you should see a new text field under the header API Key, as in Fig 9. If you don’t see your API key displayed there, you can click the button “Generate New Key” to generate a new key.

Fig 9. User Edit Page with the button to generate a new API key

Step 3. Create API Controller to Authenticate Requests

Run the following command in the terminal to create API controller which is used to authenticate requests. The two controllers for API which we will later generate will inherit from this API controller.

rails g controller api/v1/api
Fig 10. Generate API controller to authenticate requests

Then add the code in Fig 11 to app/controllers/api/v1/api_controller.rb.

Fig 11. Code in the API controller

The code in Fig 11 is mainly doing the following:

#authenticate: call authencate_user_with_token to check if a user can be found based on the token passed, and we set @user to be the returned user, or call handle_bad_authentication to render json with a 401 status code and a message of “Bad credentials” if no user is found in authenticate_user_with_token method.

In the case of missing record, we’ll handle it with “rescue_from” and render a 404 status code and a message of “Record not found”.

Step 4. Create API Endpoints for Events

One thing to notice here is that we are reusing the existing models (User and Event) for our API endpoints, so we mostly just need to add controllers and views for the API endpoints.

It’s possible for us to just reuse the existing Users controller and Events controller, but it’s considered as a bad practice. Like mentioned here, we wanna access our api via /api/v1 url, which means that our controllers for API endpoints should be placed in app/controllers/api/v1.

Now let’s run the following two commands in the terminal to generate the two controllers for the API endpoints. The result is in Fig 12.

rails g controller api/v1/events
rails g controller api/v1/users
Fig 12. Generate two controllers for the API endpoints

Enter the code in Fig 13 for EventsController. You can refer here for the code.

Fig 13. Code in app/controllers/api/v1/events_controller.rb

Now let’s add the route for the events_controller in config/routes.rb, as in Fig 14, after our existing routes.

Fig 14. Add the route for the events_controller

To verify the new routes, let’s run rails routes in the terminal. If you see something like Fig 15, it means that our new routes should be working.

Fig 15. Verify our new routes in the terminal

We need to test our API endpoints next. You can use either curl in the terminal or Postman for this task. I will be testing using Postman.

After you download and install Postman (and of course after you have started your Rails Server), enter http://localhost:3000/api/v1/events in the browser, select GET in the dropdown menu for HTTP verbs, and click the Send button. Refer to Fig 16 for locations of these items.

Fig 16. Enter the URI for index action of events in Postman and click Send

We’ll see a 401 error with a “Bad Request” message as a response, as shown in Fig 17. This is because of the method handle_bad_authentication, which is used to handle requests to our API endpoints without the API key defined in Fig 9. Now let’s copy the API key from the User Edit page (Fig 9), and then paste it to the text box Token after you select Bearer Token from Authorization menu, like in Fig 18 and Fig 19.

Fig 17. Bad credentials message received in Postman
Fig 18. Select Bearer Token in Authorization menu
Fig 19. Paste your API key to the highlighted box after Token

Now if we click Send button on Postman again, we’ll notice a 204 status code with no content returned. According to API Handyman, what 204 status code means is:

The 204 (No Content) status code indicates that the server has successfully fulfilled the request and that there is no additional content to send in the response payload body.

Fig 20. 204 status code returned

Well, we have not provided a view page for the index action of the Events API yet. So let’s fix that now. We’ll use jbuilder for this task. It’s not the most popular gem for this task, but it’s versatile and provides us with plenty of flexibilities of what to return for each model (I am interested in only returning username, instead of emails, for creators of events for the sake of privacy).

To create the view for the #index action for Event model, let’s add index.json.jbuilder file in app/views/api/v1/events. Before we add any concrete content to this view page, we’ll see a 200 status code with empty JSON returned in Postman, as in Fig 21.

Fig 21. 200 status code with empty JSON returned in Postman

events#index action and events#show action

We’ll create a partial for the event, _event.json.jbuilder in app/views/api/v1/events, as in Fig 22. What the code does is to display event.id, display the creator’s username of the event for creator, event.event_date and event.location for the event. Then we call the event partial in the view for index action in app/views/api/v1/events/index.json.jbuilder to display the @events we created in Fig 13.

Fig 22. code for _event.json.jbuilder in app/views/api/v1/events
Fig 23. Use the event partial in app/views/api/v1/events/index.json.jbuilder to display @events

Now if we click the Send button again in Postman, we’ll finally see some pretty JSON displayed, as in Fig 24.

Fig 24. Some sample JSON displayed for index action of the Event model

Let’s do the same for the show action. Create a new view file show.json.jbuilder in app/views/api/v1/events, and add the following code:

json.partial! 'api/v1/events/event', event:@event

The simple line of code is just to reuse the event partial and pass the @event we defined in Fig 13. Now if we change the URI in Postman to http://localhost:3000/api/v1/events/1 (you can change the event ID to whatever ID you see in Fig 24). Something similar to Fig 25 should be displayed in your Postman.

Fig 25. Sample JSON for show action of the Event model

events#create action

We won’t need to create views for events#create action in Fig 13 because we’ll just provide the needed event_date and location attributes for the new event in Postman. Like shown in Fig 26, if we add attributes of event_date and location as JSON in body, and choose POST verb from the dropdown menu of HTTP verbs, and then enter http://localhost:3000/api/v1/events for the URI. A new event will be created if you click Send button. You should see something similar to Fig 27 in your Postman. To get a proper datetime in the future for Ruby, you can enter something like “3.months.from_now” in the Rails Console.

Fig 26. Enter the two attributes of the new event as JSON
Fig 27. A 201 status code with new event JSON displayed

events#update action

Like events#create action, we won’t need a separate view for events@update action. We’ll just provide the new desired attribute value in JSON and select PATCH verb and enter http://localhost:3000/api/v1/events/:your_event_id for the URI, like in Fig 28. After you click Send button, you should see a JSON response body similar to Fig 29.

Fig 28. Enter your new attribute value and select PATCH for HTTP verb
Fig 29. Response body from events#update action

events#destroy action

We don’t need to provide views for events#destroy action either. To delete an event by using API endpoints, we just need to choose DELETE verb for HTTP verbs, and then enter http://localhost:3000/api/v1/events/:event_id for URI. For example, if we want to delete event with ID 7, which can be seen by events#index action in Fig 30.

Fig 30. Details of the event with ID 7 from events#index action before the deletion

Now let’s delete the event by clicking the Send button, shown in Fig 31.

Fig 31. Delete event with ID 7 in Postman

Now let’s check if the event still exists by entering http://localhost:3000/api/v1/events/7 and selecting GET for HTTP verbs, as shown in Fig 32. We’ll see an error message of “Record not found” with status code 404 displayed.

Fig 32. Check if the event is deleted by events#show action

events#show_attendees action

Now let’s add the view for events#show_attendees action. Let’s create a file show_attendees.json.jbuilder in app/views/api/v1/events. Add the code shown below to the view file.


json.attendees @attendees do |attendee|
json.username attendee.usernameend

But be noted that the events#show_attendees action is not a REST action, so we need to create a route for it. Let’s add the following route in config/routes.rb:

get 'api/v1/events/:id/attendees', to: 'api/v1/events#show_attendees', format: :json

To check if everything is working, let’s enter http://localhost:3000/api/v1/events/:event_id/attendees for URI and select GET for HTTP verbs. If you have some users registered for the event, you should see a list of users similar to Fig 33. Otherwise, you’ll see an empty array.

Fig 33. A list of users as the event attendees for an event

events#register action and events#deregister action

The two actions let users register events created by other users provided that they have not registered yet, and deregister events which they have registered (and which were created by other users).

We won’t need to create views for the two actions because we’ll just provide the event ID in the URI but we need to create two routes for them first. In config/routes.rb, add the following two routes:

post '/api/v1/events/:id/register', to: 'api/v1/events#register', format: :jsonpost 'api/v1/events/:id/deregister', to: 'api/v1/events#deregister', format: :json

Please be noted that we are using POST verb for these two routes. Now let’s test if the two actions work.

In Postman, enter http://localhost:3000/api/v1/events/:event_id/register (please pay attention to enter an event ID which you have not registered yet), and select POST for HTTP verbs, and then you’ll see status 200 with the event details after you click Send button. Fig 34 is what you see after a successful event registering.

Fig 34. result from a successful event registering

Now we’ll click the Send button again and see what happens for an unsuccessful event registering. Something similar to Fig 35 should pop up in your Postman. This is the result of the else clause for events#register action in Fig 13.

Fig 35. result from an unsuccessul event registering

We’ll do the same for events#deregister. Enter http://localhost:3000/api/v1/events/:event_id/deregister for URI and select POST for HTTP verbs, and you’ll see something similar to Fig 36 in your Postman. Just reuse the same event_id which is registered (because you have done so in Fig 34.

Fig 36. result from a successful event#deregister action

Let’s check the result from an unsuccesfull events#deregister by clicking Send button again. You should see a custome error message shown in Fig 37, which was defined in the else clause of events#deregister action in Fig 13.

Fig 37. result from an unsuccessful events#deregister action

Step 5. Create API Endpoints for Users

Since we have already created the Users controller of API endpoints in Fig 12, we’ll just add the code for the Users controller here. Please refer to Fig 38 for the code. Be noted that we only have actions users#show, users#update, and users#show_registered_events, and we’ll only allow users#update and users#show_registered_events if the passed user ID in params is the same as your own user ID.

Fig 38. Code in the UsersController

Then we’ll add new routes in config/routes.rb. Add the highlighted line after existing resources for events nested in the namespace :api and namespace :v1, as shown in Fig 39.

Fig 39. Add resources for users in the nested namespace

Then add the following route in an empty line:

get '/api/v1/users/:id/events', to: "api/v1/users#show_registered_events", format: :json

To test the API endpoints for the User model properly, you need to know your user ID. Since it’s not showing anywhere in the front end, we’ll need to get it in our Rails Console. You can run “User.all” to get a list of all the users (including their user IDs). In my case, my user ID. Let’s turn to Postman to test what we have so far.

users#show action

After we enter http://localhost:3000/api/v1/users/:user_id for URI and select GET for the HTTP verb, we see a 204 status No Content, displayed in Fig 40 . It’s because we don’t have a view for the users#show action yet.

Fig 40. 204 status code returned

We’ll create a view page for users#show action, app/views/api/v1/users/show.json.jbuilder, and then add the code in Fig 41 to the view page.

Fig 41. Code for app/views/api/v1/users/show.json.jbuilder

Now if we click the Send button in Postman again, we are expected to see a JSON file similar to Fig 42 displayed. Please be noted that my current user ID is 4.

Fig 42. A sample JSON displayed for the users#show action

users#update action

Now if we try to test users#update action to the user with ID 1 (my current user ID is 4), when we enter http://localhost:3000/api/v1/users/:user_id for URI and select PATCH for the HTTP verb, we’ll see a response similar to Fig 43 with status code 401 and an error message “Unauthorized”.

Fig 43. A status 401 and an error message “Unauthorized” response when trying to update another user’s profile

Before we move on to test with our user ID, we’ll add the view page for users#update action first. Create the view page show_self.json.jbuilder in app/views/api/v1/users. Then add the code in Fig 44 to the view page.

Fig 44. code for app/views/api/v1/users/show_self.json.jbuilder

Now if we click the Send button again, we should see a response similar to Fig 45, with a 200 status code and updated user profile with the new attribute value (here I have username as Jane Doe).

Fig 45. Response from a successful users#update action

users#show_registered_events

Finally our last action. For this action, we’ll return a list of events that the user has registered. We’ll create a view page for the action, app/views/api/v1/users/show_registered_events.json.jbuilder. Then add the code in Fig 46 to the view page.

Fig 46. code in app/views/api/v1/users/show_registered_events/json.jbuilder

Now if we enter http://localhost:3000/api/v1/users/:user_id/events for URI and select GET for HTTP verb, and click Send button, we should see a response similar to Fig 47 in Postman. Please be noted that Fig 47 shows the expected response with a successful action of users#show_registered_events.

Fig 47. A sample response from a successful users#show_registered_events action

If we try to do the same to a different user ID, we’ll see a response similar to Fig 48, with a status code 401 and an error message “Unauthorized”.

Fig 48. A sample response from an unsuccessful users#show_registered_events action

A detailed list of all the code changes for this part can be found here and here.

--

--

Brenda Zhang

A backend developer, mainly using Ruby on Rails for the time being, a lifetime learner.