For my Flatiron School Sinatra portfolio project I built a web application called ‘My Reading List’, which allows users to create an account and then keep track of books they’d like to read. They can browse the application library (which contains all books added by all users) and then add an existing book to their personal reading list or create a brand new book to add. It was a little intimidating starting the project completely from scratch, but I had a lot of fun working through it and learned a lot along the way. Now that I’m finished, it’s so rewarding to see that I built a web app I actually want to deploy and use myself!
My first step in building the application was to set up my folder structure and add all the necessary supporting files (environment, config.ru, Gemfile, etc.). This forced me to conceptualize how I wanted the application to function and the different gems, models, etc. I’d need to add to accomplish that. I started by adding a Gemfile to my project directory. I knew that at the very least I would need need to add the ‘sinatra’ and ‘activerecord’ gems (this is a Sinatra based app, after all), so I added those first. Next I added the following gems: ‘rake’ (for assistance in creating the database tables and to allow for opening a pry console for testing), ‘require_all’ (so I could simply tell my app within the environment file to load all files in a specific directory), ‘shotgun’ (so I could view the site on a local server and see any changes I made in real time), ‘pry’ (so I could play around with my objects and test different methods), ‘bcrypt’ (for encrypting the user’s passwords in the database), ‘rack-flash’ (so I could add flash messages for indicating success/failure of certain actions of the user), and ‘sqlite3’ (the database I’m utilizing to persist the data in the app).
After running ‘bundle install’, I moved on to my environment and config.ru files. In my environment file, I required ‘bundler’ (to require all of the gems in my Gemfile), established the connection to my database (to allow for persisting of the data added to my application), and required all files in the ‘app’ directory. In config.ru, I required the relative environment file and then told the application to use ‘Rack::MethodOverride’, which would allow for using the patch and delete requests in my controllers, for editing or deleting an existing object’s data. Then I told the application to use the various controllers (Users, Books, and Authors), which all inherit from the main Application controller, and last but not least, to run the Application controller.
Next step was to add the ‘app’ directory and all necessary subdirectories (controllers, models, and views). I knew my next step would be working on my models, defining the classes and relationships between them, so I went ahead and added a file for each of my models within ‘app/models’ directory (so, user.rb, book.rb, author.rb, and book_user.rb as I knew I wanted to establish a many-to-many relationship between books and users). I also went ahead and added the controller files in the ‘app/controllers’ directory (application_controller.rb, authors_controller.rb, books_controller.rb, and users_controller.rb).
As I’m utilizing the ActiveRecord gem, the code for defining my models is pretty simple. All I really had to do was define the model (i.e. class Author, etc.) and have the class inherit from ActiveRecord::Base (i.e. class Author < ActiveRecord::Base), which gives the model access to a variety of pre-defined CRUD methods (i.e. create, find, destroy, etc.). Next I added the code to each model which defined the relationships between various models (i.e. Author has_many books, Book belongs_to Author, User has_many books through book_users). I also added ‘has_secure_password’ to my User model, which works with the ‘bcrypt’ gem for setting and authenticating encrypted passwords.
At this point, I knew I’d need a method for turning model names into ‘slugs’ (i.e. removal of spaces and special characters to be replaces with dashes), to be utilized in the resource portion of the URI. Since for the most part all models would share these methods, I decided to set this up as a module called Slugifiable (in ‘app/models/concerns’ directory). At first, I included both an instance method (i.e., def slug) for turning a name into a slug and a class method (def find_by_slug) for finding an object based on it’s slugified name. However, I quickly realized that the instance method would not work for all models - a book does not have name attribute but instead a title and for a user we would want the resource in the URI for their user show page to be based on the username not their name. So instead I defined the unique slug method within each of the models, and then left only the ‘find_by_slug’ class method in the Slugifiable module, and then extended that module to each of the models.
Once my models were all set up and I had throughly tested them in the pry console (to ensure they were collaborating and responding to all methods as expected), I moved on to adding my ‘db’ directory, creating the database, and creating the migrations to set up the necessary tables in the database. I utilized ‘rake’ for this setup. I created my database (my_reading_list.sqlite) with the command ‘rake db:create’ and then created the migration files with ‘rake db:create_migration NAME=‘. After adding in the code for creating each individual table into the migration files, I ran the command ‘rake db:migrate’ and then opened my database file in DB Browser for SQLite to confirm all the tables had been created with all their correct columns.
Next step was getting to work on the controllers and views files. I found it easiest to work on one controller at a time and then add/write the corresponding view files as I wrote actions that rendered them (i.e. whenever I wrote a route that included a line with ‘erb :’, I’d go ahead and add and write out that erb file). This step of creating the controllers and views was the most time consuming as I was constantly going back and forth between writing the underlying code and playing around on the site in shotgun to ensure everything was working and appearing exactly how I wanted it to. I’m not going to go too into detail about my controllers and views, as I could go on for a while detailing it all, but I’ll add a few key points below.
I defined a main application controller (i.e. app/controllers/application_controller.rb) which basically handled the configuration and defining of helper methods (i.e. logged_in? and current_user), required ‘rack-flash’ to allow for flash messages to appear on the page (so I could add success/failure messages when certain conditions are met), and handled the controller action for ‘get’ requests to the route ‘/‘, which would render the apps landing page (i.e. the page the welcomes a user, if they’re not already logged in, and prompts them to log in or sign up). All other application controllers inherit from this main controller, so they have access to all of this as well.
I started with my main Application controller first (i.e. app/controllers/application_controller.rb) as I knew this would be the simplest controller, since it’d really just contain the route for rendering the landing page and defining any necessary helper methods. First thing I did was require ‘rack-flash’ so that all my controllers would have the ability to display flash messages. Then I configured the controller so that it would know where to get the styling files (i.e. set :public_folder, ‘public’), where to get the views files (i.e. set :views, ‘app/views’), to enable sessions (i.e. enable :sessions) and to use the ‘rack-flash’ gem (i.e. use Rack::Flash). I also defined my two helper methods in this controller - ‘logged_in?’ and ‘current_user’. The ‘logged_in?’ method uses the session hash to validate that a user is in fact logged in. This method is used throughout the application to ensure that certain routes cannot be accessed by a user who is not logged into an account. The ‘current_user’ method uses the ‘user_id’ from the session hash to determine who is the user currently logged in to the application. This is utilized throughout the application to ensure that only an appropriate user can see certain information (i.e. this is often used to check if the current user is the admin, in which case they are given additional privileges from a standard user, such as ability to delete other user accounts). I made all other controllers inherit from the Application controller so they would have access to these methods as well.
I have 3 additional controllers in my application - for users, books and authors. The users controller handles the ‘get’ and ‘post’ requests to the ‘/signup’ route, the ‘get’ and ‘post’ requests to the ‘/login’ route, and the ‘get’ requests to the ‘/users/:slug’ and ‘users/:slug/delete’ routes which render the users show page and delete a user from the application, respectively. I’ve used dynamic routes here so that whatever value is entered in the URI in the location of ‘:slug’ can be pulled from the params hash and used in conjunction with the ‘find_by_slug’ method to find the corresponding user. My books controller handles all requests related to the CRUD actions that can be taken on book objects - i.e. viewing all books or an individual book show page, creating a new book, editing an existing book, and deleting a book. I also have an action in here for adding a book to a users reading list. The authors controller is pretty simple as the only actions I’m allowing on authors is basically viewing the full authors list and viewing an individual author’s show page (i.e. a ‘get’ request to the route ‘/authors’ and a ‘get’ request to the route ‘/authors/:slug’, respectively). I also have an action for handling a ‘get’ request to ‘/authors/:slug/delete’, which allows for deletion of an author from the application, but this action is only available to the admin user (more on this next).
As I tested around with my app, I decided to add some extra features that would only be accessible to an ‘admin’ user. I originally built the app so that no user could delete a book or author from the application itself (only remove books from their personal reading list). As books can belong to multiple users (i.e. multiple users can have the same book in their reading list), I did not want to allow for the creator of a book entry to delete that entry, because then it would disappear from all other users’ reading lists, and I would find that quite annoying as a user. However, I still wanted the application to allow for deleting objects (whether its a user account, book entry, or author) within the application itself (i.e. without someone having to go into the database to delete it). I decided the best way to allow for this would be to create an ‘admin’ user and then add in some conditionals to different routes and views. To the views, I added in buttons for removing each of these objects from the application, but made these buttons visible only if the current user is the ‘admin’ user. In the controllers, I added in conditionals to different controller actions to only allow for a certain action (i.e. deleting an author from the application) if the current user is the ‘admin’ user.
The final step I took in building my application was adding bootstrap and updating my views accordingly. This took my app from looking really plain and boring, to having some basic styling which made it a lot more pleasing to look at and use.
All in all, this was a really enjoyable project to work on and it really helped to solidify my understanding of MVC architecture, RESTful routes, dynamic routes, HTTP requests, persisting data, and more. It was a valuable learning experience and I’m excited to build more web apps!