Rebuilding LoreScroll: Building A Simple API

The first major task in my attempt to build a better, more efficient and React-based version of my favorite project LoreScroll was to set up my backend. I needed an API to store the Story, Setting and Character data generated by my users that I could later fetch and display using JavaScript and React.

The following is an explanation of my process and not necessarily meant for instructional purposes. That said, feel free to glean whatever you can from it.

Creating My App

I’ve used Ruby On Rails quite a bit and already have the pre-requisites installed. As such, to get started, all I had to do was cd into my LoreScrollV2 directory and run the following code in my terminal to build out the foundation of the app.

rails new backend

I realized after generating my backend that I could have also added the api tag to the end of my code, which would have formatted my app to optimize for API functionality and likely saved me some time. That said, I wanted a bit more control over the process, so this method worked for me in the long run.

Configuration

Before I jumped into coding, I wanted to give myself access to a few things that I knew would be necessary as I continued to develop LoreScroll.

I entered my Gemfile and made a number of changes:

  • In order to allow for password encryption for my users, I uncommented the bcrypt gem.
  • To assist with making AJAX calls, I added the rack-cors gem.
  • To simplify serializing my data, I added the fast-jsonapi gem.
backend/Gemfilegem 'bcrypt', '~> 3.1.7'
gem 'rack-cors'
gem 'fast_jsonapi'

To complete my configuration, I also added the following middleware code to my cors initializer — changing the origins to my server: localhost:3000.

backend/config/initializers/cors.rbRails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000'
resource '*', headers: :any, methods: [:get, :post, :patch, :put]
end
end

Modeling and Migrating

With my foundation laid, I began building the base models for my minimum viable product (there will likely be many more before I’m done): User, World, Setting and Character. I did this using the Model generator discussed in my previous post. This provided me with four empty models and four migrations that would allow me to construct my database.

##rails generate User username:string password_digest:string email:string penname:stringclass User < ApplicationRecord
end
class CreateUsers < ActiveRecord::Migration[6.1] def change
create_table :users do |t|
t.string :username
t.string :password_digest
t.string :email
t.string :penname

t.timestamps
end
end
end
-----------------##rails generate Story title:string genre:string story_type:string summary:string user_id:integer setting_id:integerclass Story < ApplicationRecord
end
class CreateStories < ActiveRecord::Migration[6.1]
def change
create_table :stories do |t|
t.string :title
t.string :genre
t.string :story_type
t.string :summary
t.integer :user_id
t.integer :setting_id

t.timestamps
end
end
end
-----------------##rails generate Setting name:string summary:string user_id:integerclass Setting < ApplicationRecord
end
class CreateSettings < ActiveRecord::Migration[6.1]
def change
create_table :settings do |t|
t.string :name
t.string :summary
t.integer :user_id

t.timestamps
end
end
end
-----------------##rails generate Characterclass Character < ApplicationRecord
end
class CreateCharacters < ActiveRecord::Migration[6.1]
def change
create_table :characters do |t|
t.string :name
t.string :role
t.string :age
t.string :gender
t.string :species
t.string :job
t.string :physical
t.string :personality
t.string :history
t.string :motivation
t.integer :setting_id

t.timestamps
end
end
end

Relationships and Validations

You’ll notice that when I generated my Models and Migrations, I included foreign keys on each of my tables to allow for table relation. That’s because I had already done some legwork before I started coding and determined the logic of my relationships.

  • A User has many Stories and has many Settings. This is relatively self explanatory.
  • A Story belongs to a User and belongs to a Setting. At this point, I’m not allowing for any kind of collaboration, so a Story belongs to a single user. The reason a Story belongs to a Setting — instead of the other way around — is because a setting can be featured in multiple stories. For example, Tatooine appears multiple times throughout the Star Wars saga. Setting will be the overarching object that ultimately includes many more specific types of setting objects.
  • A Setting has many Stories and has many Characters. It belongs to a User. This set up will eventually allow a Character to have many Stories through Setting, though that is a relationship I have yet to develop.
  • A Character belongs to a setting. This is also self explanatory. A character exists in a setting and exists in a story through that setting.

The next step was to include these relationships within my models.

class User < ApplicationRecord
has_secure_password

has_many :stories
has_many :settings

validates :username, presence: true, uniqueness: true, length: { minimum: 7 }
end
-----------------class Story < ApplicationRecord
belongs_to :user
belongs_to :setting
validates :title, presence: true
end
-----------------class Setting < ApplicationRecord
has_many :stories
has_many :characters
belongs_to :user
validates :name, presence: true
end
-----------------class Character < ApplicationRecord
belongs_to :setting
validates :name, presence: true
end

Beyond the relationships, I also included has_secure_password, a feature of bcrypt, in my User model to allow for password encryption. F

Finally, I added some basic validations to prevent any bad data from getting entered into the database — i.e. if I’m displaying a story’s title, I don’t want there to be no information to pull from. In the case of User, a username must be present, it must be unique and it must have a minimum of seven characters. In the case of Story, Setting and Character, they must have an identifying name or title present.

This will all come in handy when creating forms for data entry, which I’ll likely go over in a future blog.

Routing

With my basic Models, Migrations and Relationships complete, I was able to migrate my tables (rails db:migrate) and create my ActiveRecord database. The next thing to think about was routing and building controllers to handle any routing actions.

To start, I added my routes paths to the routes.rb file in my config directory. I elected to stick with three simple routes to start for each of my models: index, show and create. While I will eventually add an option to edit and delete content, I want to get the rest of my API up and running first.

I also decided to namespace my routes as an :api, since that is the primary function of this Rails application. This means that when I call a route in my browser it will need to be preceded with ‘/api’: (i.e. https://localhost:3000/api/stories).

Rails.application.routes.draw do
namespace :api do
resources :users, only: [:index, :show, :create]
resources :stories only: [:index, :show, :create]
resources :settings only: [:index, :show, :create]
resources :characters only: [:index, :show, :create]
end
end

Serializing Data

Serializers allow us to create a custom structure for our data, helping us to improve upon the suboptimal default that Rails provides for APIs. For example, we know that each User object is going to have several associated Stories. Serializers give us the ability to access assigned data for those stories directly from the User object, which is much easier way of grabbing that data.

Since I knew I wanted to serialize my data, I elected to create my serializers before my controllers. With the fast-jsonapi gem already installed, I was able to access generators to speed up this process.

##rails generate serializer User username email penname stories settingsclass UserSerializer
include FastJsonapi::ObjectSerializer
attributes :username, :email, :penname, :stories, :settings
end
-----------------##rails generate serializer Story title genre story_type summary user settingclass StorySerializer
include FastJsonapi::ObjectSerializer
attributes :title, :genre, :story_type, :summary, :user, :setting
end
-----------------##rails generate serializer Setting name summary stories characters userclass SettingSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :summary, :stories, :characters, :user
end
-----------------##rails generate serializer Character name role age gender species job physical personality history motivation settingclass CharacterSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :role, :age, :gender, :species, :job, :physical, :personality, :history, :motivation, :setting
end

There files will appear in the serializers folders within the app directory.

We’ll see how all of this turns out once we’ve got our controller up and running, but the important thing to understand is that relational data is now nested and stored with an object. For example, if we are looking at the json of a Setting object, the Story and Character objects related to that setting will be nested and made available for easy access. This is an incredibly useful tool!

Building Controllers

The last big step in building our API is to create our controllers, which will provide the code necessary to complete the action defined in our routes: index, show and create.

To ensure our namespacing functions correctly, we want to create an api folder within our controllers directory. This is where we’ll place all of our files. We also want to make sure to precede our class names with “Api::” as this will give us access to render json data.

Rather than show you every controller I’ve created, let’s focus on a single one — as the information contained within each is fairly similar when constructing an API.

app/controllers/api/StoriesController.rbclass Api::StoriesController < ApplicationController   def index
@stories = Story.all
render json: StorySerializer.new(@stories)
end
def show
@story = Story.find(params[:id])
render json: StorySerializer.new(@story)
end
def create
@story = Story.new(story_params)
if @story.save
render json: StorySerializer.new(@story), status: :created, location: @story
else
render json: @story.errors, status: :unprocessable_entity
end
end
private def story_params
params.require(:story).permit(:title, :genre, :story_type, :summary)
end
end

Let’s break down our controller action methods:

  • Index: This is designed to render all of the stories in our database. We assign Story.all to an instance variable called stories. Then, we render json using our newly created StorySerializer with the method .new and pass in our stories variable.
  • Show: This is designed to render a single instance of a Story object. We find the Story using its id parameter and assign it to an instance variable called story. Then, as with index, we render json using StorySerializer.new — this time passing in the story variable.
  • Create: This allows us to add a new Story object to our database. Eventually we’ll build a form in React that utilizes this action. First we create a new Story object and pass in our story_params (which we will discuss in a second), assigning what’s return to an instance variable of story. If our story saves successfully, we’ll render json using StorySerializer.new and pass in the story variable. If it doesn’t save, it will throw an error.
  • Story_Params: Finally, within a private method, we’ll create our strong params — which is necessary to maximize the security of data that we pass through forms, which we know we’ll want to do eventually. Story_Params will include all of the attributes we know we’ll want the user to have access to.

There’s room to expand our controller with edit and destroy actions, but this looks great to start with!

Wrapping It Up

We’re almost done! To test out my API and ensure it was working properly, I seeded the database with some dummy data using “rails db:seed” and ran “rails server” to test out the results.

Let’s see what we’ve got:

https://localhost:3000/api/stories{
"data": [
{
"id": "1",
"type": "story",
"attributes": {
"title": "Star Wars",
"genre": "Science Fantasy",
"story_type": "Screenplay",
"summary": "The Imperial Forces -- under orders from cruel Darth Vader (David Prowse) -- hold Princess Leia (Carrie Fisher) hostage, in their efforts to quell the rebellion against the Galactic Empire. Luke Skywalker (Mark Hamill) and Han Solo (Harrison Ford), captain of the Millennium Falcon, work together with the companionable droid duo R2-D2 (Kenny Baker) and C-3PO (Anthony Daniels) to rescue the beautiful princess, help the Rebel Alliance, and restore freedom and justice to the Galaxy.",
"user": {
"id": 1,
"username": "Leonardo",
"password_digest": [FILTERED],
"email": "leonardo@tmnt.com",
"penname": "Leo Turtle",
"created_at": "2022-04-27T00:55:57.685Z",
"updated_at": "2022-04-27T00:55:57.685Z"
},
"setting": {
"id": 1,
"name": "A Galaxy Far, Far Away...",
"summary": "A far off galaxy held together by a mystical power called 'The Force'.",
"user_id": 1,
"created_at": "2022-04-27T00:55:58.359Z",
"updated_at": "2022-04-27T00:55:58.359Z"
}
}
},
{
"id": "2",
"type": "story",
"attributes": {
"title": "Eberron",
"genre": "Fantasy",
"story_type": "Roleplaying Game",
"summary": "Eberron is a campaign setting for Dungeons and Dragons. Eberron is set in a period of healing after devastating 102 year war that ravaged the continent of Khorvaire and split the mighty kingdom into 12 quarreling nations.",
"user": {
"id": 2,
"username": "Donatello",
"password_digest": [FILTERED],
"email": "donatello@tmnt.com",
"penname": "Donny Turtle",
"created_at": "2022-04-27T00:55:57.902Z",
"updated_at": "2022-04-27T00:55:57.902Z"
},
"setting": {
"id": 2,
"name": "Rising from the Last War",
"summary": "A world where magic power technology.",
"user_id": 2,
"created_at": "2022-04-27T00:55:58.370Z",
"updated_at": "2022-04-27T00:55:58.370Z"
}
}
}
]
}

There’s our json! Our API is up and running successfully!

We can really see the impact of the serializers when viewing this file. Not only do we have access to all of the Story’s attributes, we also have access to the attributes of the associated User and Setting objects.

Building an API with Ruby On Rails is fairly simple once you’ve got a handle on the process. While it will certainly grow with the rest of LoreScroll as I add features, this is enough to get started on my front end. We’ll discuss that next week!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store