In the last article, we covered how JWT (JSON Web Token) works. In order to implement JWT into our endpoints, we need to add it in as meta data for our responses. To achieve that, we will need to implement serializers for our Rails API.
Feel free to refer to the part 8 branch of the GitHub repository if needed.
This is the 8th installment of my Developing A Cross-Platform iOS & Android Social Media App series.
Before we setup serializers, we should take a look at what a serializer does and why we need it to implement JWT. Serializers formats our data prior to outputting the response. It keeps our response clean, and allows us to add meta data for access tokens (JWT). Here is our current successful /login
response:
{
"updated_at" : "2020-02-10T18:09:33.148Z",
"created_at" : "2020-02-10T18:09:33.148Z",
"email" : "[email protected]",
"id" : "50cc4c1f-aa26-4a99-a1e3-e345f5ba58ee"
}
If we implement a user
serializer, we can have it automatically format that response to this:
{
"user" : {
"id" : "50cc4c1f-aa26-4a99-a1e3-e345f5ba58ee",
"email" : "[email protected]"
},
"meta" : {
"access_token" : "123"
}
}
Our user data is now under a user
attribute. Notice how we no longer have updated_at
or created_at
. In some scenarios, we never use that information so there is no need to output it and make the response larger.
We also have the meta
attribute which allows us to append the access_token
to our response. The frontend can then extract that access_token
in a clean way. Here is an example of how you could extract data from the response above using javascript:
const {user, meta} = response;
const {access_token} = meta;
Serializers are much more powerful than selecting which attributes to output. We can also output relations, such as the first 3 comments for a post. Using serializers keeps our responses small and allows us to combine multiple responses into one. For example, not having to hit /comments
endpoint to get the first 3 comments for each post in the /posts
endpoint.
We would need to first setup serializers. Add gem 'active_model_serializers'
to your Gemfile
and run $ bundle install
:
...
gem 'jwt'
gem 'active_model_serializers'
...
We will now need to create an initializer that will configure active_model_serializers
every time the server is ran. Initializers are located under config/initializers
. Lets create one called active_model_serializers.rb
. You can create the file via terminal by using the touch
command: $ touch config/initializers/active_model_serializers.rb
. Add the following lines to your new initializer:
ActiveModel::Serializer.config.adapter = :json
ActiveModelSerializers.config.key_transform = :underscore
Lets break this down line by line, starting with:
ActiveModel::Serializer.config.adapter = :json
ActiveModel::Serializer
: This is the class that active_model_serializer
gem provides.
config
: This is the config class that we can use to set our configurations.
adapter
: This is a setting to determine what our adapter is. Adapters choose how our responses should be serialized. By default it uses the attributes
serializer.
:json
: This is the adapter we will use that formats our response as JSON.
ActiveModelSerializers.config.key_transform = :underscore
key_transform
: This setting allows us to configure our json keys to be CamelCase
, lowerCamelCase
, underscore_case
(snake_case
), dashed-case,
or unaltered
.
:underscore
: we will be using underscore for our keys. This is becuase the Ruby language uses underscores and it would make it easier if data being sent, manipulated, and recieved had consistent key names. Even though our frontend is in Javascript, which uses camelCase, we won't be doing any manipulation on our frontend. Sorting, searching, and etc will all be done in the backend Rails API. I do this to remove any complexity out of the app and make updates easier when it comes to anything data related.
Create a serializers
directory:
$ mkdir app/serializers
Create a v1
directory:
$ mkdir app/serializers/v1
Create a user_serializer.rb
file under app/serializers/v1/
:
$ touch app/serializers/v1/user_serializer.rb
Add the following to the file:
class V1::UserSerializer < ActiveModel::Serializer
attributes (
:id,
:email
)
end
V1::UserSerializer
: Similar to controllers, this declares a class named UserSerializer
under v1
. This tells us that this serializer will be used by controllers under v1
.
ActiveModel::Serializer
: The class will inherit the ActiveModel::Serializer
class.
attributes
: These are the attributes that will be used in the serialization.
:id
, :email
: These would be the only two columns we care about right now. We won't need created_at
or updated_at
for this endpoint.
Now that our serializer is setup, we can test it out by calling the login
endpoint. The controller will automatically look for the UserSerializer
under v1
. There is a way to manually set the serializer that you want to use, for example a different user serializer, but we will cover that in the future.
We can now test the serializer. Call the login endpoint:
$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 125 0 74 100 51 241 166 --:--:-- --:--:-- --:--:-- 242
{
"user" : {
"id" : "7545d290-962f-45bb-891d-f7e43b4fbf68",
"email" : "[email protected]"
}
}
Sweet! Our API now only returns the attributes that were specified in our serializer.
Serializers give us the option to add meta data. This would be the extra data appended to our response. In this case we want to return our JWT access token. Since we have not worked on generating a token in the controller yet, we will just hardcode a string.
Take a look at the render method of your login endpoint in your app/controllers/v1/user_controller.rb
file:
return render json: user,
status: 200
We have the option of passing a hash as a value for the meta
key. Create a hash with a mock access_token
and set it to the meta
key:
return render json: user,
meta: {access_token: '123'},
status: 200
Now we can test the login endpoint again:
$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 155 0 104 100 51 735 360 --:--:-- --:--:-- --:--:-- 737
{
"user" : {
"id" : "7545d290-962f-45bb-891d-f7e43b4fbf68",
"email" : "[email protected]"
},
"meta" : {
"access_token" : "123"
}
}
Our meta deta is now stored under the meta
key in our JSON response. We are now ready to create our JWT module in the next section.