Building Bag and Board: My First Sinatra App

R. Cory Stine
20 min readApr 23, 2021

--

There’s something really exciting about opening a browser, firing up a fresh server using shotgun, and seeing your first web application come to life. While I learned a lot through the process of building my CLI project, it was an often tedious process where I was constantly questioning my knowledge and didn’t always know why certain solutions worked and why others didn’t. It was more trial and error than informed problem solving. But with my Sinatra project, I felt like I was more prepared. The blank screen was less scary at the beginning and when I did run into an error, I knew how to interpret it. It was very freeing and helped to build my confidence as a programmer.

Again, I wanted to build something that I could use in my every day life, because if I can solve a problem of my own, I can solve it for others too. The idea came to me one day when I was organizing my room and moving a long box of comics into my closet. When you collect comics, it can get overwhelming. You can have dozens, hundreds, even thousands of books to keep track of. I wanted to build an app where you could easily construct a digital comic book library for yourself, view details about each book and see what other people are reading. That was the genesis of Bag and Board.

I have a few too many of these things (though definitely not this one).

I started by doing some basic research: checking out how other people had structured their projects from a UI perspective, watching a few walkthroughs on Instruct and reviewing some of the labs that had led up to the project. My primary goal was to create an MVC Sinatra app using ActiveRecord that would allow a user to sign up, login and logout, as well as implement CRUD (create, read, update, destroy) methods to manipulate and change an object — in my case a comic book.

I mapped out a basic concept using draw.io. I knew I would need two models to start — a User and a Comic. A User would have many comics and a Comic would belong to a user. The User model would have three attributes: a username, an email address and a password (or password_digest, since I would be using the bcrypt gem for validation and authentication). The Comic would require several: a title, the issue number, the writer(s), the artist(s), and a rating. I also chose to include an option of a story arc to more easily define a comic beyond its simple issue number. For example, the “Court of Owls” story arc in Batman ran from issues #1–7.

From there, I began to map out my routes and ERB pages. I knew I would need an index page to act as a home, where the user could either sign up for a new account or login to their existing account.

The sign up page would require a username, a unique email and a password and if all the requirements were met and the account successfully posted, the user would be redirected to a user’s show page (more on that in a second).

The login page would require a username and password. It would validate the password to ensure account security. If the login failed or not enough information was provided, the user would be redirected to sign-up. Otherwise, on a success, the user would be logged in and a new session would begin.

The user’s show page would display their personal comic book collection in total and provide them with an option to add a new comic to their collection or click on an individual comic to view its details. The new comic page would open a form allowing them to enter details of a new comic and on submit, post that comic to their library, opening up a comic show page to display those details. The comics show page would also include the option to edit or delete that comic. If the user elected to edit, they would be taken to a separate edit form, which would then be patched back onto the comics show page, changing any of the edited details. The delete button would destroy the object altogether and return them to the main library, where it would no longer be visible on their list of comics.

My original conceptualization of the app.

Ultimately, this wasn’t the final structure I settled on, but it was a good guide to get me started.

I chose to use the Corneal Gem to generate the skeleton of my app. While I believe I could have set up my models, views and controllers from scratch, it was nice to have them formatted in a way that felt familiar. It took some fiddling with the Gemfile to get Corneal working properly, resetting the version of some gems and deleting others, but it was worth learning how to troubleshoot my way through an issue like that.

Signing Up, Logging In and Logging Out

My first programming task was to get my sign up and login flow functioning correctly. I built out a migration table for my users, which contained all of the necessary attributes (see above) for a single User. To make sure I could properly associate my prospective models, I also set up my comics table migration. There, I included a column called user_id that I would later use to connect the two tables together through association.

class CreateUsers < ActiveRecord::Migration[5.2]   def change
create_table :users do |t|
t.string :username
t.string :email
t.string :password_digest
end
end
end
-----class CreateComics < ActiveRecord::Migration[5.2] def change
create_table :comics do |t|
t.string :title
t.integer :issue
t.string :arc
t.string :writer
t.string :artist
t.integer :rating
t.integer :user_id
t.timestamps null: false
end
end
end

The models themselves were fairly straight forward creations. My User model has_many :comics and includes the method has_secure_password which would allow me to authenticate passwords in future routes. Later on, I would also add an email validation that would ensure that at sign-up, the e-mail address the user entered was unique and that the form was not empty. This would help prevent bad data from getting entered into the database. My Comic model belongs_to :user, completing the association. To test that my migrations had worked correctly, I created some seed data and opened tux to verify that the data was properly aligned with each column.

With my database functioning, it was time to build my controllers and views so that that the data could be accessed through a display. I started by creating a simple index route in my ApplicationController that would get my index.erb view. There, I stubbed out two links in HTML — one that would direct the user to a login page and another that would direct them to sign up for a new account. I also set up a UserController file that would contain all of my routes pertaining to the user data — starting with a get route for both the login and signup views. I added this new UserController to my config.ru to make sure it would load with the rest of the application.

The next step was to construct my forms using HTML and ERB. I set the names of my inputs to equate to the names of the columns on each table so that my params calls would be easy for me to remember (i.e. name=“username”, name=“password”, etc).

<h1>Bag & Board</h1>
<br>
<h4>Sign Up For Your Account!</h4><br>
<form action="/signup" method="post">
<label for="username">Username:</label>
<input type="text" name="username" id="username"><br><br>
<label for="email">Email:</label>
<input type="text" name="email" id="email"><br><br>
<label for="password">Password:</label>
<input type="password" name="password" id="password"><br><br>
<input type="submit" value="Create Account">
</form>

With both the login and signup views completed, I could fire up a server and see both of the forms appear on my screen. I was slowly, but surely, getting online. But I could only view the forms — not use them to post new content. For that I would need some additional code.

I navigated back to my ApplicationController, where I enabled sessions in my configuration. I also added two helper methods that I knew would help me with validations in the future: logged_in? and current_user. The latter would find a user by the current session id, verifying that they were, in fact, the current user. If current_user returned as true, that means they would be logged in, allowing them to see content only available to users within their account.

helpers do
def logged_in?
!!current_user
end
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end

With that, I was able to begin work on a set of post routes that would allow the user to login and/or sign up for a new account. To login, I would first find the user by their e-mail using User.find_by(email: params[:email]) and set that to an instance variable called @user. The params key would be set by the name of the form input type. To authenticate the password, I would use the method .authenticate, made available through bcrypt, within the context of an if statement. If @user was true and params[:password] could be authenticated, then the session’s id would be set to @user’s id — starting a new session- and the user would be redirected to their personal library show page. Otherwise, they would be sent back to login to try again.

post '/login' do
@user = User.find_by(email: params[:email])
if @user && @user.authenticate(params[:password])
session[:user_id] = @user.id
erb :"users/show"
else
redirect to '/login'
end
end

The signup post route would be fairly similar, but instead of simply finding the user, I would create a new one using the params keys from the sign-up form (User.create(username: params[:username], email: params[:email], password: params[:password]). If the user could save, meaning that they had filled out the form and their e-mail address was unique — set using the validation method in the User model, then a new session would be created and they would be redirected to their personal library show page. If not, they would be sent back to the sign up page to fix any entry errors.

With my views and post routes complete, I could now sign up for a new account and login with that data. Because of my authentications and validations, I could not login without the proper credentials, ensuring security. The last step would be to set up a log out route. In this case, I implemented a log out link on the user’s show page (which to this point had been largely meant for testing). If the user clicked this link and they were logged in, their session would be cleared, logging them out and redirecting them to the login page.

Wednesdays Are For New Comics

With the nuts and bolts of my account system functioning, it was time to dig into the fun stuff: creating a personal comics library, adding comics to that library, and being able to edit or delete those comics.

This is only here because I love the gif. Take a break, enjoy.

To implement my vision for the comics section of the app, I actually had to finish up with the UserController by fleshing out my user’s show page. I wanted it to plainly display the user’s comic book collection, so I used ERB tags to iterate through each of my user’s comics, displaying their title and issue number. The get ‘/users/:id’ route in UserController would find a User model using params[:id] and then open that user’s show page.

With a small library displaying on the user’s show page, the app already had some of its basic functionality. However, I also wanted to be able to add a new comic to the library, edit it, and delete it if necessary — to fully utilize CRUD principles.

My new comic page was not unlike the forms I had created to sign up or login to the site. Initially, I set it up using names that corresponded to the attributes in the Comic model: title, issue, arc, writer, artist and rating. Most of these would be text inputs, but I wanted to ensure that the user could only enter an integer for the issue number, preventing any glaring formatting issues. To accomplish this, I set the input type to “number” and set a minimum value of “0” to keep the user from entering a negative value. I also thought it would be best to standardize the rating system using a drop down menu that would inform the user of what each number represented in terms of quality. I did some research and determined that a select tag, in conjunction with a series of option tags would be my best option.

Later in the process, I elected to add some additional features to this form: a publisher and a summary/review. I knew I would need to add some columns to the comics table, so I created two new add_column migrations, coinciding with those fields. I implemented a second select tag to track specific publishers like DC, Marvel and Inage. For the review field, I used a textarea tag, giving the user a bit more room to work with than the standard block text field.

###The final result.<h1>Bag & Board</h1>
<br>
<h4>Add A New Comic</h4>
<br>
<% if flash.has?(:message) %>
<%= flash[:message] %>
<% end %>
<form action="/comics" method="post">
<label for="title">Title:</label>
<input type="text" name="title"><br><br>
<label for="issue">Issue Number:</label>
<input type="number" min="0" name="issue"><br><br>
<label for="publisher">Publisher:</label>
<select name="publisher">
<option value="DC">DC</option>
<option value="Marvel">Marvel</option>
<option value="Image">Image</option>
<option value="Dark Horse">Dark Horse</option>
<option value="IDW">IDW</options>
<option value="DC Vertigo">DC Vertigo</options>
<option value="Boom Studios">Boom Studios</options>
<option value="Valiant">Valiant</options>
<option value="Dynamite">Dynamite</options>
<option value="Oni Press">Oni Press</options>
<option value="Other">Other</options>
</select>
<br><br>
<label for="arc">Story Arc:</label>
<input type="text" name="arc"><br><br>
<label for="writer">Writer(s):</label>
<input type="text" name="writer"><br><br>
<label for="artist">Artist(s):</label>
<input type="text" name="artist"><br><br>
<label for="rating">Rating:</label>
<select name="rating">
<option value="10">10 - Masterpiece</option>
<option value="9">9 - Incredible</option>
<option value="8">8 - Great</option>
<option value="7">7 - Very Good</option>
<option value="6">6 - Pretty Good</option>
<option value="5">5 - Okay</option>
<option value="4">4 - Not Great</option>
<option value="3">3 - Bad</option>
<option value="2">2 - Terrible</option>
<option value="1">1 - An Abomination</option>
<option value="0">0 - Not Yet Read</option>
</select>
<br><br>
<label for="review">Review/Summary:</label><br>
<textarea name="review">Enter text here...</textarea><br><br>
<input type="submit" name="submit" value="Create Comic">
</form>
<br><br>
<a href="/users/<%= @user.id %>"><h3>Return Home</h3></a>

To access this view, I created a new ComicsController, which would account for any routes pertaining to the Comic model. The first route would be to get ‘/comics/new’, retrieving the new comic form erb file and displaying it to the user. I would include a link to this route on the user’s personal library page, so they would be able to easily recognize how to add a new comic to the library.

The post route for the new comic would create a new Comic object, passing in its attributes and setting them to the associated params key (i.e. title: params[:title]). Then, it would set the new comic’s user id to the current user’s id, giving the User ownership over the Comic. All of this would be saved and the user would be redirected to the comic’s show page, which would display all of the data collected for a specific comic — using HTML and ERB tags. To allow for this, I also created a new route in the ComicsController to get ‘/comics/:id’.

Adding a new comic and seeing it appear in the user’s library was thrilling and my first real hint that, at least from a CRUD perspective, I was moving in the right direction. But I still needed to add an edit and a delete option — which I had sometimes struggled with during labs in the past.

I built out an edit form, which was not dissimilar from the new comic form. The only major difference being that I chose to use ERB tags to include the pre-existing data for each attribute as a value in the form field. This would prevent the user emptying out the entirety of their entry, even if they only wanted to edit a single field. I also formatted the names of the params keys a bit differently, so that they would not exactly match the params from the new route — structuring them as such: comic[title].

Back in the ComicsController, I added two new routes. The first was to get the edit form using ‘/comics/:id/edit’. Then, came the patch, where I had often tripped up in previous lessons. I used my post route as inspiration, finding the comic by its ID, then individually setting each of that comic’s attributes to the information in the associated params hash and saving the end result. I would then redirect the user to comic’s show page to display the applied changes. To my surprise, it worked on the first try, which felt like a major win after slogging through patches in the past. To make the form and patch route accessible, I added a link to the form at the bottom of the comic’s show page.

Finally, it was time to set up an option to delete a pre-existing comic. I added a basic delete form to my comic show page, then constructed a delete route in my ComicsController. It would find the comic using its ID and set it to a variable of @comic, then execute the .delete method on the @comic variable to delete that instance of the Comic object. Afterward, it would redirect the user back to their user show page, displaying their library without the deleted comic — verifying it had been removed.

delete '/comics/:id' do
@comic = Comic.find_by_id(params[:id])
@comic.delete
redirect to "/users/#{@comic.user_id}"
end

A simple version of my app was complete (and ridden with bugs). I could login to an account, then create, read, update and delete a comic book from my ActiveRecord database. Amazing! But there was still work to be done.

Inking the Linework…OR How I Learned to Stop Worrying And Love Validations and Bug Fixes

Bag and Board was functioning, but with a whole lot of problems. Users could edit or delete comics that weren’t their own, viewing someone else’s account would occasionally switch you to their library — effectively overriding the security at login, and many of the redirects just didn’t seem to make sense with the flow of the app. It was time to buckle down and work on some bug fixes and detailing.

What I look like while I code.

Before I got too in the weeds, I had an idea that was a bit outside of my original plans, but that I believed would add a new scope to the project and solve some of the flow issues I was having. If the concept of Bag and Board was to encourage people to try new comics, then the best way to do that would be to show them what other people were reading on the app — a Universal Comics Feed. The user would login to their personal library, but also have an opportunity to checkout the titles, ratings and reviews in other user’s libraries as well. This would act as a sort of default redirect option if I didn’t want to send a user back to their own show page or a specific comic book.

To accomplish this, I created a new get route in ComicsController for ‘/comics’ that would contain a variable called @comics and would sort every comic in the database by their title, providing an alphabetized list of everything available.

@comics = Comic.all.sort_by{|h| h[:title]}

In the feed’s view, I used ERB tags to iterate through the @comics variable: selecting each comic, wrapping it in a link directing it to the proper comic show page and printing out specific details pertaining to that comic in the library. I started with the name and issue number, but ultimately determined that was not enough to get a user clicking on someone else’s content. As such, I would later add the comic’s rating, who entered into the database, as well as the name of the writers and artists on the book. This would display as an unordered list to the user, giving them a chance to browse through a complete accounting of every comic in the database.

<h3>The Universal Feed</h3>
<br>
<% @comics.each do |comic| %>
<ul>
<a href="/comics/<%= comic.id %>"><b><%= comic.title %> #<%= comic.issue %></b></a> - Rating: <b><%= comic.rating %></b> - Added by: <%= comic.user.username %>
<br>
<h6>---Written by: <%= comic.writer %>, Art by: <%= comic.artist %></h6>
</ul><br>
<% end %>

Onto the bug fixes!

While my signup and login posts already had validations and authentication, if bad or incorrect data was entered, it simply redirected the user back to the exact same page without any indication of what had gone wrong. This did not feel like the best user experience, so I decided it would be best to use the flash gem, with accompanying flash messages, to at least give the user a sense of what they needed to do to fix the problem. Adding the flash messages to the routes, then placing them within the appropriate views, was an easy fix that made a huge difference to the final product.

Remaining in the UserController, I wanted to prevent logged in users from manually returning to the login or signup page by entering the route into the URL bar in their browser. While that seemed like it would be an edge case scenario, it could be a fairly easy mistake and potentially cause some problems with maintaining or logging out of a session. To solve this issue, I fell back, as was so often the case, on my helper methods. If a user attempted to navigate to the ‘/login’ or ‘/signup’ routes and they were logged in, they would be redirected to ‘/comics’, my new Universal Comics Feed view. Elsewise, they would be taken to the corresponding form view, as expected.

I used similar conditionals to add some additional security to the ‘/logout’ and ‘users/:id’ routes. If a user attempted to navigate to ‘/logout’ through the URL bar and they were not logged in, they would be redirected to ‘/’ (or the index) — giving them the option to login or sign-up once more. Otherwise, their session would be cleared and they would be redirected back to ‘/login’, completing the logout process. A user could only visit the ‘users/:id’ route if they were logged in and the current user was equivalent to the user found with find_by_id. This would prevent a user from adding a new comic to another person’s library.

get '/logout' do
if !logged_in?
redirect to '/'
else
session.clear
redirect to '/login'
end
end
get '/users/:id' do
@user = User.find_by_id(params[:id])
if logged_in? && current_user == @user
erb :"users/show"
else
redirect to "/comics"
end
end

The ComicsController would need significantly more work. I started with the simplest requirement first — I knew I didn’t want to allow a user who was not logged in to view much of the comics content at all. So, for routes like ‘/comics’, ‘/comics/new’ and ‘/comics/:id’, I implemented a pattern. If the user was logged in, I would run the necessary code to load those pages, otherwise they would be sent back to ‘/login’ to try again.

I also wanted to add some additional requirements to the post and patch routes that would create a new comic or allow for it to be edited. Namely, I wanted to make sure that every comic had a title. The rest of the information wasn’t necessarily mandatory — a user might not know the name of the writer or the artist, for example, but I did not want a bunch of blank comics cluttering my database. It would make the feed impossible to use and be unsightly to the user. To accomplish this, I wrapped my existing code in a conditional. If the title was not blank, the code would execute. If not, the user would be redirected to ‘/comics/new’ (or ‘/comics/:id/edit’) and receive a flash message: “A comic title is required to add it to your library.”

if params[:title] !=""
#existing code here
else
flash[:message] = "A comic title is required to add it to your library."
redirect to "/comics/new"
end

Everything was starting to come together, but the more I fixed, the more I discovered. There were still some pretty glaring issues that were becoming more evident as I tested things in the browser:

  • If a user viewed someone else’s comic, they could edit it or delete it without much trouble at all.
  • The delete button seemed to only work haphazardly. Sometimes it would delete an object, other times I would receive an error in the browser. There wasn’t much logic to this.
  • There were a lot of situations in which I was redirecting to the ‘/comics’ feed when it would probably make more sense to send the user back to their personal library.

To address the first issue, I knew I would need to add some code to my get ‘/comics/:id/edit’ and delete ‘/comics/:id’ routes. It seemed to me that the best way to validate permissions to edit or delete content would be to check to make sure that the current user’s ID matched the user ID of the comic. If they matched, the edit form view would display. Otherwise, they would be sent back to the comics feed.

get '/comics/:id/edit' do
@comic = Comic.find_by_id(params[:id])
if logged_in? && current_user.id == @comic.user_id
erb :'comics/edit'
else
redirect to "/comics"
end
end

While this solved the actual issue, it still felt strange that the user would even see the option to edit or delete content that did not belong to them. So as a fail safe, in the comics show view, I wrapped the existing edit link and delete form in an additional conditional statement using ERB tags. That HTML would only display if the current user’s ID matched the comic’s user ID. If not, that section of the page would remain blank. This would ensure that these options were not even available to the user to start with, nor could they be navigated to in the URL bar.

With that resolved, it was time to move on to the delete bug. Because it was so erratic, I had some difficulty identifying the issue, but as I’ve found is often the case with coding, the final solution was subtle. On the comic show page, within the delete form, I had set the action to “/comics/<%= @comic.user_id %>”. Rather than the comic’s ID, I had set it to delete using the comic’s user ID. So, for example, if the comic’s ID was 16, but the comic’s user ID was 2, then it would attempt to delete the wrong object! All I needed to do to fix this was to change the action to the proper link to my action: “/comics/<%= @comic.id %>”.

My last major bit of business had more to do with the overall flow of the app, as opposed to any specific bug. I was constantly redirecting the user back to the feed view, even though their library was their “home” page so to speak. I had done this largely as a temporary solution because it was easier than interpolating the user ID into a redirect link over and over again. My initial thought was to proceed with the interpolation, but the more I mulled it over, the more I realized that while the personal library was an integral part of the application’s usefulness, its real function was to serve as a community where comic book readers could share and review new issues and recommend them to others. The feed page was the core of the experience and would be better off as the home page. As such, I shifted the code towards these aims, cleaning up the feed view to make it feel like more of a landing page and changing some of my linking structure to make the Universal Feed the centerpiece of the app.

Lessons Learned

I didn’t quite stop there. I continued to play around with the way in which my data would display on the feed, the personal library and the comics view. I added details like the date a comic was added to the database. But much of the work from this point forward was cosmetic. The actually requirements of the assignment had been met.

This Sinatra project was the first time I feel like I’ve had genuine fun coding. There were moments of frustration, for sure, but I knew I had the tools to push through those moments. And I looked forward to sitting down and seeing what I could accomplish with each session at the keyboard. Hell, sometimes I didn’t even want to stop. I feel much more confident in my knowledge of Ruby, Sinatra, HTML and ActiveRecord after completing Bag and Board and am genuinely proud of my work.

Even with the project completed, I’m already brainstorming ways in which to expand the apps’ functionality. Perhaps adding a Writer or Artist model that would allow the user to see which comics I specific talent has worked on. Or a means of sorting the comics feed alphabetically, by date added or by other functions. I’d also like to go back and use slugs instead of ID numbers in my routes.

Regardless of what form it takes next, I’m excited to continue my coding journey into the next section of the Flatiron course.

--

--

No responses yet