My good friend and coding partner in crime, Jordan, is building a Pokedex app using React. She's a CSS and design whiz! Check her out! Anyway, when we pair she does the fancy artsy magic and I tackle the APIs. So when she asked me to make a rails API for her Pokedex blog project I was more than happy to oblige!
I talked it over with her to find out exactly what she wanted. She wants users to be able to signup, login, and add pokemon to their decks with persistence. So my thought is to have a User model, a Pokemon Model, a joins table called Pokedex that will have a has-many-through relationship between a User and a Pokemon. Using draw.io I made up the relationships I'm envisioning to get her approval:
For my user authentication, I will use sessions and cookies. There are lots of opinions on whether this is the best choice versus using JSON web tokens, but I find sessions and cookies easier to navigate and since it's an app that I don't anticipate having huge amounts of users and it's strictly for fun, I'm not too concerned about CSRF attacks. That being said, this is why you don't reuse passwords people! Even though I will be using Bcrypt to hash the passwords and I won't know what you entered, it's always a good idea to use different passwords because you never know when an app isn't using good authentication practices. Ok, I'm going to get off my soapbox, and time to start building!
Firstly I wanted to make sure I was on the same page as Jordan, so I navigated to our collab channel on Github and cloned a copy of her personal-pokedex project.
In my own terminal I made a folder to hold both the back and front end projects called pokedex and ran:
$ git clone https://github.com/Jordeks/personal-pokedex.git $ cd personal-pokedex $ yarn install $ yarn start
This allowed me to see what her front end is looking like right now and I can see how she has structured her components.
All right! Time to start on the API!
~/.../pokedex/personal-pokedex // ♥ > cd .. ~/.../post-grad/pokedex // ♥ > rails new pokedex-api --api --database=postgresql
Once Rails had completed its process of generating the files, I made a new git repository in Jordeks:
Then made my initial commit and it's time to rumble!
Let's add some gems that we'll need; go ahead and un-comment out
gem 'bcrypt', '~> 3.1.7' gem 'rack-cors'
in the Gemfile. We'll need bycrypt to set up password protection for users and rack-cors so the front end can make requests to the backend. Run
$ bundle install
so they are added to the Gemfile.lock. While we are thinking about cors, let's go to config/initializers/cors.rb and comment in:
Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end
For your origins, you can use a '*' which is the wildcard and will allow for any URL to send requests, or you can specify which local ports you might use while in development and also later add the deployed URL.
I also know I will want to use a serializer to send the data as JSON so let's add the rails active model serializer:
$ bundle add active_model_serializers
After talking over what Jordan wants, I came up with this plan for making the models and tables:
Using $ rails g resource, I can generate migrations, models, controllers, serializers, and routes using the following commands (for Pokedex, I don't anticipate needing a list of all the joins, so I'm just going to generate a model for it):
$ rails g resource User username password_digest $ rails g resource Pokemon name p_id:integer image_url $ rails g model Pokedex user:belongs_to pokemon:belongs_to
If the data type is a string, you don’t need to specify their type following the column name. Adding user:belongs_to specifies the relationship between your two tables and sets up a column for user_id in your pokedexes table. Additionally, we use column name password_digest to avoid directly storing passwords as strings.
Once rails is done generating, I like to go through each file that is created to make sure everything worked as expected, which means for the resources checking the routes.rb, controller, model, serializer, and the migration. I'm looking for spelling mistakes and typos in particular in the migration table before I migrate it.
Top Trick: By installing the active model serializer before I generated the resources, rails knows to automatically build the serializer for anything that has a controller. That way I don't have to go back later and manually generate the serializers. However, if you don't do this, this blog includes some great step by steps for generating serializers.
Also, when looking at the serializers that were generated, I notice that the user_serializer includes the password_digest. Under no circumstance should that be sent to the frontend where malicious users might try to access it. So let's take that out now.
class UserSerializer < ActiveModel::Serializer attributes :id, :username, :password_digest end
class UserSerializer < ActiveModel::Serializer attributes :id, :username end
Next, run $ rails db:create to create the back end and $ rails db:migrate to migrate your tables.
At this point I like to check that I can run
$ rails s
and see that it runs:
Woot! Welcome to rails! Now if I navigate to http://localhost:3000/pokemons, I get the very helpful rails error: The action 'index' could not be found for PokemonsController. Which is exactly what I'm expecting at this point.
Ok, let's go back to the models and finish adding the relationships. Pokedex already has both belongs_to relationships because we generated it with that reference.
class Pokedex < ApplicationRecord belongs_to :user belongs_to :pokemon end
Let's add in the missing has_many relationships:
class Pokemon < ApplicationRecord has_many :pokedexes has_many :users, through: :pokedexes end
And for the User model, I'm going to add the has_secure_password macro so that we can use Bcrypt to protect their password.
class User < ApplicationRecord has_secure_password has_many :pokedexes has_many :pokemons, through: :pokedexes end
My next step is always to test out my relationships in the rails console to make sure it behaves the way I want it to. And guess what! By doing this I found a typo in where I placed the colon on one of my has_manys, so it is worth the time to do this! So after testing that I can make a user, a pokemon, and associate the two to create a pokedex, we will want to create a seeds file. I like taking time to do this as it helps set up a game plan for how the controllers will work later.
jordan = User.create(username: "Jordles", password: "password") meks = User.create(username: "Meks", password: "password") bulbasaur = Pokemon.create(name: "bulbasaur", p_id: 1) ivysaur = Pokemon.create(name: "ivysaur", p_id: 2) venusaur = Pokemon.create(name: "venusaur", p_id: 3) jordan.pokemons << bulbasaur jordan.pokemons << ivysaur meks.pokemons << ivysaur meks.pokemons << venusaur
Top Trick! You can use the commands:
$ rails db:drop db:create db:migrate db:seed
All in one line to drop the database so any users you made in the console that you don't want to conflict with your seeds are removed. Then it will recreate the database, migrate and seed it all in one go.
Next, let's test that we can build an endpoint, I doubt that Jordan will ever want to return all the pokemon in the database, but it's a good place to check that we are getting the data we expect as json through our serializer.
class PokemonsController < ApplicationController def index render json: Pokemon.all, status: 200 end end
Yes! We got back an array of objects with the name, id, and an empty URL for the image. After talking with Jordan, turns out she won't ever need to get the URL from the backend, so for now, we can take that out of the serializer.
class PokemonSerializer < ActiveModel::Serializer attributes :id, :name, :p_id end
Sweet. I am happy with this.
Ok, before I get too far ahead of myself, I want to move the pokemon and user controllers into a V1 folder inside an API folder inside the controllers' folder. This is a good habit to get into so that one can easily create new versions on the API that the front end can use and the only thing the front end has to change is which version it sends requests to. So we need to create the two folders, namespace the two controllers, and update the routes.
class Api::V1::PokemonsController < ApplicationController def index render json: Pokemon.all, status: 200 end end
class Api::V1::UsersController < ApplicationController end
Rails.application.routes.draw do namespace :api do namespace :v1 do resources :pokemons resources :users end end # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end
Now I can navigate to http://localhost:3000/api/v1/pokemons and see:
This is the route that the frontend will send requests to for all the pokemon (if it ever wants it).
Next let's take care of some validations. We don't want any entries that might mess up with our database. The user absolutely must have a username, and it must be unique since that is how the will be identified upon login.
class User < ApplicationRecord has_secure_password has_many :pokedexes has_many :pokemons, through: :pokedexes validates :username, presence: true validates :username, uniqueness: true end
And Pokemon are must also have names and p_ids that are present and unique:
class Pokemon < ApplicationRecord has_many :pokedexes has_many :users, through: :pokedexes validates :name, :p_id, presence: true validates :name, :p_id, uniqueness: true end
Now if I try to create a new Pokemon without the p_id, I get an error in console:
and a successful insertion if it is saved to the database! I can also see the validations at work if I try to make a Pokemon that already exists:
By using $ p.errors.any? I can see if there was an error and the command $ p.errors.messages returns to me a list of validation errors.
If you are with me this far, give yourselves a pat on the back! We have successfully made our database, models, controllers, serializers, routes, validations, and done some manual testing in console to make sure our relationships are working. In the next rendition, we will set up our user authentication system with sessions and cookies.
You can go through the code at the Jordeks github repository.