DEV Community

Cover image for Building a Pokedex with Rails Part 1
Meks (they/them)
Meks (they/them)

Posted on

Building a Pokedex with Rails Part 1

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:

Alt Text

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.

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

This allowed me to see what her front end is looking like right now and I can see how she has structured her components.

Alt Text

All right! Time to start on the API!

~/.../pokedex/personal-pokedex // ♥ > cd ..
~/.../post-grad/pokedex // ♥ > rails new pokedex-api --api --database=postgresql
Enter fullscreen mode Exit fullscreen mode

Once Rails had completed its process of generating the files, I made a new git repository in Jordeks:

Alt Text

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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.

pokedex-api/app/serializers/user_serializer.rb

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :password_digest
end
Enter fullscreen mode Exit fullscreen mode

to:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
end
Enter fullscreen mode Exit fullscreen mode

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:
Alt Text
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
Enter fullscreen mode Exit fullscreen mode

Let's add in the missing has_many relationships:

class Pokemon < ApplicationRecord
    has_many :pokedexes
    has_many :users, through: :pokedexes
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top Trick! You can use the commands:

$ rails db:drop db:create db:migrate db:seed
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Alt Text
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
Enter fullscreen mode Exit fullscreen mode

Alt Text

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.

pokedex-api/app/controllers/api/v1/pokemons_controller.rb

class Api::V1::PokemonsController < ApplicationController

  def index
    render json: Pokemon.all, status: 200
  end

end
Enter fullscreen mode Exit fullscreen mode

pokedex-api/app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController
end
Enter fullscreen mode Exit fullscreen mode

pokedex-api/config/routes.rb

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
Enter fullscreen mode Exit fullscreen mode

Now I can navigate to http://localhost:3000/api/v1/pokemons and see:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now if I try to create a new Pokemon without the p_id, I get an error in console:

Alt Text

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:

Alt Text

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.

Happy coding!

Top comments (2)

Collapse
 
sturpin profile image
Sergio Turpín

Just what I was looking for, fantastic! Very well explained!! 👏☺️👌

Collapse
 
danhmanh profile image
Danh Manh Nguyen

Very clearly for new beginners! Keep going!