04 July 2020
Before starting to work on our business domain, we need to run rails db:create
command to create development and test databases. While creating databases, make sure that you already installed PostgreSQL, and it’s running on your computer. (You can execute pg_isready command to check)
If you didn’t review the Instapaper service so far, I would suggest to register and use it a little bit before proceeding to read this chapter.
As we can see on Instapaper, the first thing that comes to mind is creating a User
model and providing an authentication system. For the authentication, we have options like sorcery and clearance gems, but I prefer to use devise gem because it’s a battle-tested and well-known gem in the ruby community, which has the comprehensive feature set, and also it’s easy to use.
Let’s add devise gem to Gemfile
, and run bundle install
command.
After that, we need to execute the devise install command.
It creates configuration and localization files of the devise. We also need to add default_url_options
for the development environment because we need to provide all the details to generate URLs as following for mailers.
Instead of passing the URLs’ host value every time we build mailer views, we will set it once with default_url_options
.
(For now, we only set it for the development environment, but keep in mind that you need to set also for production environment when you deploy to the production.)
After that, we need to generate a User model by using the devise gem.
This command will generate a database migration, which creates users table and also the user model along with spec and factory files.
Creating the User model with the devise also modifies routes.rb
and adds devise_for :users
line, which adds routes for User resource that provides sign in, sign out, update password, and CRUD operations with view files provided by the devise. But we will not use them since we don’t need view files, and we will use those functionalities behind our API endpoints, so we don’t want to expose devise routes directly.
We created our rubocop ruleset in the previous chapter, but we will iterate the ruleset when we see the points that we want to change along the course. After creating the User model by using devise gem, we have two offenses which we need to keep or remove if we don’t see any advantage of having them.
missing top-level class documentation
cop, but personally, I don’t think it’s sustainable to maintain this ruleset. Besides that point, we should have a goal of naming classes carefully to make them easy to understand without any comment line.After adjusting those rules, we should have a .rubocop.yml
file looks like as following. (You can also remove Style/Documentation
block from .rubocop_todo.yml
file since we disabled it.)
Now, we need to run the database migration created by the devise.
Our user model is ready to use, but before using it, I would like to write specs for the user model. Some software engineers tend to use TDD, but I want to write specs after I build the intended behavior because I don’t want my software design to be affected by tests. (You can read DHH’s blog post about this topic to get more information.)
Let’s open spec/models/user_spec.rb
file, which is generated when we create the user model using the devise. Although we didn’t write validations for the user model, the devise gem automatically adds presence validation to both email and password fields. So we can add validation tests for the email and password fields of the user model.
Let’s run rspec
to make sure both of the tests are green.
Some developers might think about why we test devise internals, but we are actually testing our implementation of the devise rather than the devise gem itself. If any other developer changes the authentication library and somehow forgot to validate email and password, those tests will cover and warn the developer.
The second thought might be, “but we already don’t allow null values in database” that’s true, so it will not break our data integrity. Still, to be able to handle errors on the application layer, it’s better to validate them also on the application side, which is handled by the devise for now, but it might be different with another library.
With simple words, restful API consists of resources and interactions. We can call anything as a resource, and it’s represented by nouns like a user
, project
, etc. You can also define processes as a resource as long as you can describe them with nouns like CreatingBankAccount
. Resources don’t have to be mapped with any data source. You can think like it’s a way of defining terms in your business domain.
On the other hand, we have interactions for resources that are represented by verbs, and they are approximately mapped with HTTP request methods like GET, POST, DELETE, PUT, PATCH, etc.
So we use both resources and HTTP methods to design restful API. If we have a User resource, our API endpoints might look like this:
I tried to explain how the restful API designed without diving into details so much, but if you want to gain more information, you can check blog post from Thoughtworks about the topic.
Since we already created our User model, we can start working on user creation endpoint. As a common convention of API controllers, let’s create api/v1
folder (api folder and v1 folder inside of api folder) inside the controllers’ folder and create api/v1/base_controller.rb
extended by ApplicationController
.
Rubocop default ruleset will ask you to use nested module/class definitions instead of compact style like Api::V1::BaseController
. Even though it’s not a big deal, I want to use the compact style of the definition, so I disabled the rule in the rubocop file.
So we created a base controller for our API version one. If you’re building a restful API, it’s better to version it in advance so that you can create different folders for the next versions like v2, v3. In addition to that, we created BaseController
also because if we want to use the same abstraction (like authentication and error handling) from all controllers for API version one, we can place it to BaseController
easily.
Before building the endpoint, let’s modify the routes file to have users resource with a create endpoint as following.
Now we can build Api::V1::UsersController
with a create action.
Even though we cover success and unprocessable entity cases in the create action, we will implement shared error handling logic for more generic errors like ActionController::BadRequest
or ActiveRecord::RecordNotFound
in the next chapters. In the following chapters, we will also implement serializers
and renderer
modules, which will provide a better abstraction for rendering resources and errors. Keep in mind that the action we wrote here will be refactored after we introduce the renderer module.
Again rubocop detected Style/IfUnlessModifier
offense for the if block we didn’t write as a single line condition. We can refactor this line as return render json: @user, status: :created if @user.save
. However, I prefer having options of using both multiline and single line conditions based on code readability instead of forcing myself to use single line conditions all the time. To do that, we can remove Style/IfUnlessModifier
rule from .rubocop_todo.yml
and disable it on rubocop.yml
file as follows.
Now we can send a post request to create any user. (If you want to send the request from the web client application using the browser, it will fail because we didn’t set up CORS yet. We will also implement CORS settings in the next chapters.)
And API response should look like this:
Before jumping into another topic, let’s write the tests for the users controller. You can generate the spec file with bundle exec rails g integration_test api/v1/users
command.
Here, we’re providing parameters with valid and invalid information and checking if the API returns correct HTTP status with expected user information or errors. As you can realize, we used JSON.parse(response.body)
to match actual results with predefined expected data. It’s common to have similar tests, so we can extract the parsing body of the response part to the separate module (spec/support/json_helpers.rb)
and use it from there.
Then we need to include json helpers in spec_helper.rb
.
And now, we can refactor users spec and use load_body(response)
instead of JSON.parse(response.body)
.
In this chapter, we created a user model using devise gem and learned how to build an API endpoint for user creation. You can find the source code of bookmarker application on github. In the next chapter, we will introduce active_model_serializers for API endpoints. You can also find the previous chapter here.
Thanks for reading! I would love to hear your feedback about the chapter and the course in general. You can contact me by email or from twitter.