This past two weeks I've been working on my first full-stack application with a RESTful API I made using the Ruby on Rails framework. This project (more than the three I've worked on before this) felt absolutely HUGE to me when I was getting started. As a Ruby on Rails newbie, I had only worked with Rails applications with 2 to 3 models that mostly had a one-to-many relationships. Plus, these apps were school assignments that had a bunch of starter code!
After brut-forcing my way through getting this project off the ground (and losing a lot of sleep in the process) I eventually emerged on the other side in a state of coding zen. Below are some tips for building your own Rails API and some things I'm very happy to have learned in my process.
Pre-Planning Strategies 🗓️
The most important part about starting to build a Rails API is understanding the relationships between your models. In my opinion, everything else is secondary. I am a huge fan of regular old analog pen and paper when it comes to mapping out my database! This way I can look at it in whatever layout is pleasing to me at the time. (Sometimes my brain wants to see things vertically, sometimes horizontally. 🙃) That said, everyone is different and sometimes we'll need to collaborate on our preplanning. I have also used dbdiagram.io which is a great tool for helping to visualize the schema of your db.
Something else that I employed in this project management planning was what I've called a workflow grid. The guidelines for our project suggested to work on the project in "vertical slices" in that each function should be seen through from start to finish and then each additional function build on top of that. For example, work on the sign-in flow and see it through from migrations to client side styling before you move on to another function that might involve another model/migration.
(Something that is also really important for me is to not spend too long on the planning side of things. While it's good to have a solid plan in place and a solid understanding of where you want to end up, it's equally important to get past the initial hurdle of just getting something down on the screen! You will end up learning so much about your data while you're working - even some things that you didn't plan for. I think this is especially true when you are a beginner!)
Getting Started (For Real) 🏁
When you're ready with your models and relationships, it's time to generate some resources. You can always generate the models, migrations and serializers separately if you prefer a little more control. I considered this when I was starting. Generating a whole resource felt to permanent - like I had to have my whole life planned out in order to use it. But this is a fallacy!
Using a generator, in particular the resources generator does not mean you have to keep everything it generates! You can always go in and edit the migration file (before running the migration), you can go into the routes file and specify the routes you need, and you can choose not to use the serializer if you don't need it. Even after running the migrations, you can still add or take away or rename columns! Remember: the models and migrations and seeds make up a set of instructions for creating the database.
(A brief note on this way of thinking in general: nothing is ever "unchangeable." We go to great lengths to make sure that that is not the case. This is why we use version control software like git and structures like Active Record and Rails I used to be so scared of doing something wrong that I would agonize about doing it right the first time. Thankfully this thinking fell by the wayside about 3 days in!)
Here is an example of a resource generator for a basic Users model:
rails g resource User username password_digest image_url bio:text
(I chose to add relationships to my models directly - but you could add them to the generator too)
This will generate:
- A model: user.rb
- A migration with the specified columns (remember to add a foreign key (belongs_to) relationship to your migration before you migrate if necessary)
- A controller: users_controller.rb
- A serializer: user_serializer.rb
- A route in the routes.rb file:
resources :users
Hooray! We're in it!
Models and Migrations and Serializers (Oh my!) 🦁
So what do we have here?
Model: The model represents the data stored in your server for your application. It corresponds to a specific database table and provides any relationship information and a script for how to interact with the data.
Make sure your relationships are mapped our correctly in your model by using has_many
, belongs_to
, or has_many, through
statements. Though this doesn't affect the schema, this will affect the way the database stores and reads the data!
Migration: Migrations are used to manage changes to the database schema. They provide a way to create, modify, or delete database tables and columns. Again, this is set of instructions for the database to be created. Remember not to edit the migration files without first rolling back the database. OR write a new migration to make edits!
Serializer: Serializers transform model data into a suitable format (such as JSON) for sending over the API as responses. They allow customization of the data representation and control what attributes to include or exclude. You can include relationships in the serializer to handle nested data too. For example:
class UserSerializer < ActiveModel::Serializer
attributes :username, :image_url, :bio, :location
has_many :offerings
has_many :bids
end
This serializer will send back a nested user object with associated offerings and bids. Note that the attributes do not include the password_digest column. That is not info we'll need to pass from the backend ever.
Lastly we have a controller: The controller handles the incoming HTTP requests from clients, performs necessary actions, and generates responses. It serves as the middle-man between the client and the model. This is how your frontend communicates with your backend.
Each model has it's own controller but you can also choose to outsource some reusable actions to a parent application_controller such as error handling.
Route. Route. (Let it all out) 🛣️
These are the things I can do without...
The resources generator will automatically add the 'resources' route to the routes.rb file for that particular resource. However, be sure to remove the routes you won't need. Having extra routes can cause security issues, slow down your application, and make it harder to test your routes that you do need.
Make sure to use Postman to test your routes and error handling while you're building them. I literally forget that Postman exists every time I sit down to work on routes. But when I remembered, boy did it help! Having (or choosing really) to use your frontend to test a simple route really slows things down.
Validations and Error Handling ⚠️
Validations are the best way to protect your database and make sure that only the data that is supposed to be saved is saved! Even if it is just a presence: true
validation. I would recommend validating almost every piece of information that the user is asked to input.
I choose to put my error rescue in the application controller along with the authorized method to keep my code DRYer.
class ApplicationController < ActionController::API
include ActionController::Cookies
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
before_action :authorized
def authorized
render json: { errors: ["Not Authorized"] }, status: :unauthorized unless session.include? :user_id
end
def render_unprocessable_entity_response(exception)
render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity
end
end
And don't forget to use the bang(!) operator in your controller methods when you create! or save! to the database. The bang operator will raise an exception if the creation of the record fails (because of validations) which will allow for the custom error handling in your render_unporcessable_entity_response
method.
byebug 👋
Don't forget about byebug! Sometimes I'm guilty of trying to debug my application by using only the front-end. But the more I needed to debug, the more the front-end only method wasn't cutting it. Using a byebug in your controller action can let you explore what is happening after each step before that response is sent to your frontend.
In the same vein, don't forget about the console! Using the command rails c
in your terminal and exploring your data from time to time to make sure that everything looks ok. You can always write some class methods in your models to help with data exploration too.
Ask for Help! 🆘
Sometimes learning requires us to struggle through some challenges from time to time. However, sometimes the most valuable lesson can be to ask for help on something that really has you stuck. (I always need this reminder!)
Get Some Rest 💤
That's it. That's the tip.
I found that there were times where my brain stopped making sense and I would spend hours on one bug. I can't tell you how important it became to recognize when I had reached a point of diminishing returns while working on this size of a project and needed to call it quits for the day. Almost every time I woke up fresh in the morning and fixed the bug right away!
Good luck out there! We've got this.
Top comments (1)
Awesome stuff! Very splendid article. Can't wait for more!
Here are some take aways from my experience.
Create! Skips your before_create validations which is something I wasn't aware of.
Also your errors in the App controller may also be moved to an error handler module. Why? Well you will see as ur application grows that u will need to have a lot of error handling.
Versioning your APIs too is nice. 😊
Don't forget the good old bullet gem for those pesky N+1 problems.