DEV Community

Cover image for File Structure Basics for a Ruby API
Cody Barker
Cody Barker

Posted on • Updated on

File Structure Basics for a Ruby API

Are you excited to create your own back-end using Ruby, but overwhelmed with the myriad files and folders required to get it up and running? You're not alone, as many new developers feel the same when trying to setup their own API for the first time. The typical file structure contains many files and folders, and it can be a challenge to remember what exactly each of them is responsible for. With so many gems available, it can be difficult to know just which ones to choose for your project. In this guide, I'll walk you through the basic file structure and boilerplate code necessary to get your server working, so you can connect to a front-end for a full-stack application.

To begin, let's first take a look at the general file structure of a Ruby server. While most servers will have some additional files and complexity, we're going to build ours using the Active Record, Sinatra, SQLite3 and Rake gems to keep things simple and efficient. We'll also be using Ruby 2.7.4 for this guide as it works with the gem versions defined in the Gemfile.

Basic File Structure

▾server
   ▾app
     ▸controllers
     ▸models
  ▾config
    database.yml
    environment.rb
  ▾db
    ▾migrate
       migration files
       schema.rb
       seeds.rb
  Config.ru
  Gemfile
  Rakefile
Enter fullscreen mode Exit fullscreen mode

Server

We'll start with a root "server" folder so we can keep the front-end and back-end separate in our app. You can name this whatever you like.

Gemfile

Before we dive into the rest of the file structure, let's take a look at what we're going to include in our Gemfile. The Gemfile will contain all of the gems we intend to use, which will give us some context as we continue building out our back-end. Perhaps the two most important gems we'll be using are Active Record and Sinatra. Active Record contains a number of built-in methods for creating and managing our databases. It also helps us make the correct associations between our classes and our databases by following "convention over configuration." So long as we follow the proper conventions in Active Record, we can dramatically reduce the amount of code needed to hook everything up. Sinatra simplifies the dynamic routing of our controllers so that it becomes easy to handle CRUD operations from client requests, and partners with Active Record to update our databases.

To create the Gemfile, enter "bundle init" in the terminal in your project directory. For the sake of this guide, we'll be using the following gems:

You may also need to install SQLite3 via WSL on Windows or Homebrew on OSX.

For WSL Users: in the terminal, enter "sudo apt install sqlite3".

For OSX Users: If you have Homebrew installed, you can simply enter "brew install sqlite".

source "https://rubygems.org"

# Facilitates the dynamic routing and CRUD of our controllers
# https://github.com/sinatra/sinatra
gem "sinatra", "~> 2.1"

# A fast and simple web server
# https://github.com/macournoyer/thin
gem "thin", "~> 1.8"

# Parses the body of requests into params for use in controllers
# https://github.com/rack/rack-contrib
gem "rack-contrib", "~> 2.3"

# Allows us to load resources from foreign servers     
# https://github.com/cyu/rack-cors
gem "rack-cors", "~> 1.1"

# An object-relational mapper which streamlines database mgmt. 
# Utilized primarily in our database and models folders.
# https://guides.rubyonrails.org/active_record_basics.html
gem "activerecord", "~> 6.1"

# Configures common Rake tasks for working with Active Record
# https://github.com/sinatra-activerecord/sinatra-activerecord
gem "sinatra-activerecord", "~> 2.0"

# Helps us run common tasks from the command line
# https://github.com/ruby/rake
gem "rake", "~> 13.0"

# Allows us to interact with our SQLite3 databases
gem "sqlite3", "~> 1.4"

# Require all files in a folder
gem "require_all", "~> 3.0"

# These gems will only be used locally
# "pry" will allow us to enter the console
# "rerun" will automatically reload the server whenever it changes 
group :development do
  gem "pry", "~> 0.14.1"

  # https://github.com/alexch/rerun
  gem "rerun"

# These gems will only be used when if/when running tests
group :test do
  gem "database_cleaner", "~> 2.0"
  gem "rack-test", "~> 1.1"
  gem "rspec", "~> 3.10"
  gem "rspec-json_expectations", "~> 2.2"
end
Enter fullscreen mode Exit fullscreen mode

App

Our first child folder will be called "app", which will house our "controllers" and "models" folders. We'll follow the Model, View, Controller concept and separate our controllers and models. To learn more about MVC, checkout this resource.

Models

Our "models" folder will house our "model" files. These are ruby files that house the classes we intend to use to represent our real life data. These model names must follow a few important conventions to work properly with Active Record.

  1. The model name must ALWAYS be singular, so that it can associate correctly to pluralized tables in the database. For example, if we create a "messages" table in our database, we must name our corresponding model file "message.rb" with a class of "Message" that inherits "ActiveRecord::Base".
  2. Keep the file name lower-cased and snake_cased.
  3. Keep the model class name PascalCased.

At it's most basic, an example message.rb model file would look like this.

class Message < ActiveRecord::Base
end
Enter fullscreen mode Exit fullscreen mode

Controllers

Within our controllers folder, we might have one or more controller files. If using just one for a simple app, it's fine to name it something like "application_controller.rb". This file will handle the dynamic routing for server requests and should be defined as a class which inherits "Sinatra::Base". Since we'll be handling JSON data with the controller, we'll set the default content type to 'application/json'. The controller will have CRUD methods for handling client requests to a resource in our database (a plural table name associated with its singular model name) and might look a little something like this.

class ApplicationController < Sinatra::Base
  set :default_content_type, 'application/json'

  get '/messages' do
    messages = Message.all
    messages.to_json
  end

  post '/messages' do
    message = Message.create(
      body: params[:body],
      username: params[:username]
    )
    message.to_json
  end

  patch '/messages/:id' do
    message = Message.find(params[:id])
    message.update(
      body: params[:body]
    )
    message.to_json
  end

  delete '/messages/:id' do
    message = Message.find(params[:id])
    message.destroy
    message.to_json
  end

end
Enter fullscreen mode Exit fullscreen mode

Config

Note this is the folder, not the config.ru file, which we'll get to. This config folder contains two files, "database.yml" and "environment.rb".

database.yml

The database.yml file is where we set up our connection between Active Record and the database. In our case, it relies on the sqlite3 gem to act as our default adapter. We'll use just a little bit of code to set this up. Pool handles the number of connections to be made with the database and the timeout sets how long too wait before attempting to reconnect to the database. The important thing here is that our adapter is set to sqlite3 and the database paths are matched to the correct environments.

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3
Enter fullscreen mode Exit fullscreen mode
environment.rb

This file is responsible for requiring in our gems and all of the files in our app directory. It's also responsible for creating the environment variable used by our rake tasks to determine whether we're in our development, test, or production environment. It looks like this.

#creates the environment variable for "rake"
ENV['RACK_ENV'] ||= "development"

#requires our gems
require 'bundler/setup'
Bundler.require(:default, ENV['RACK_ENV'])

#requires all the files in our 'app' directory
require_all 'app'
Enter fullscreen mode Exit fullscreen mode

config.ru

Note that this file is NOT in our config folder. It lives in the top directory, as a child of our "server" folder. This file is responsible for requiring the environment file we just created, as well as configuring our Rack commands when our application runs, hence the .ru file extension for "rackup". I know these config files might be a bit confusing, but if you set it up this way, you'll be just fine.

require_relative "./config/environment"

# allow CORS requests
use Rack::Cors do
  allow do
    origins '*' #change origin to your own when deploying!
    resource '*', headers: :any, methods: 
      [:get, :post, :delete, :put, :patch, :options, :head]
  end
end

# parses JSON data from the body of requests into the params hash
use Rack::JSONBodyParser

# our application
run ApplicationController
Enter fullscreen mode Exit fullscreen mode

Rakefile

Rake is a gem that will help us run common tasks from the command line. The Rakefile is where we will define those tasks so we can easily start our server, set which port we want it to run on, and access the console whenever needed. It will also require the environment file we just created, and rake from /sinatra/activerecord/rake to work.

require_relative "./config/environment"
require "sinatra/activerecord/rake"

desc "Start the server"
task :server do  
  if ActiveRecord::Base.connection.migration_context.needs_migration?
    puts "Migrations pending. Run `rake db:migrate` first."
    return
  end

  # rackup -p PORT will run on the port specified (9292 by default)
  ENV["PORT"] ||= "9292"
  rackup = "rackup -p #{ENV['PORT']}"

  # rerun auto-reloads the server when files are updated
  # -b runs in the background (include this or binding.pry won't work)
  exec "bundle exec rerun -b '#{rackup}'"
end

desc "Start the console"
task :console do
  ActiveRecord::Base.logger = Logger.new(STDOUT)
  Pry.start
end

Enter fullscreen mode Exit fullscreen mode

db

At long last, our database folder. This folder is home to our migrations, schema file, and seed file.

migrate

We're using SQLite3 and Active Record to create and interact with our database. All of our migrations will be housed in this folder. Whenever we create a new migration, say to add a new table, to change a column title, or to drop a table, it will be created within this directory. Migrations allow us to easily keep track of our database changes, rollback any changes, recover data that was previously deleted, and work with the same database structure across teams of engineers. Migrations are great!

Our migration files should always be created from the command line, using rake, so they are correctly timestamped and tracked. It's also a good idea to call them with "bundle exec rake" to avoid any errors.

To create our "messages" table, we would run the following in our CLI:

bundle exec rake db:create_migration NAME=create_messages
Enter fullscreen mode Exit fullscreen mode

It's important to leave the migration file name and class name alone. If you start messing with either of them, it can cause problems. In order to create our "messages" table, we might have a migration file that looks something like this:

class CreateMessages < ActiveRecord::Migration[6.1]
  def change
    create_table :messages do |t|
      t.string :body
      t.string :username
      t.timestamps
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we're creating a table called "messages" (note the pluralization), with column names of "body" and "username" which will both accept strings of data, and timestamps (for when the data was created and when it was last updated). If we followed the conventions as outlined in the section on models, this table will correctly associate with our "message" model.

schema.rb

Our schema file will be automatically generated with our first migration, and it will show us the current "schema" or structure of our database. To make our first migration, in the CLI we'll run:

bundle exec rake db:migrate
Enter fullscreen mode Exit fullscreen mode

Our schema file should now look a little something like this:

ActiveRecord::Schema.define(version: 2023_04_28_193159) do

  create_table "messages", force: :cascade do |t|
    t.string "body"
    t.string "username"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

end
Enter fullscreen mode Exit fullscreen mode

Any time we rollback migrations, or create new migrations and migrate from the CLI, this file will update. This is how developers keep track of the database structure to ensure they're all in sync.

seeds.rb

Our seeds.rb file is where we can create dummy data to fill our database for testing. It's helpful for teams to use the seeds file because it provides the template for generating new testing data, quickly and efficiently. I'd recommend looking into using the Faker gem and loops for creating all sorts of useful, seed data in larger volumes, but for simplicity's sake we'll hard code a few messages here.

puts "🌱 Seeding messages..."

Message.create([
  {
    body: "Hi there!",
    username: "Cody"
  },
  {
    body: "Hello!",
    username: "Kelli"
  },
  {
    body: "Do you want to have dinner at Gino's tonight?",
    username: "Cody"
  },
  {
    body: "I love Gino's!",
    username: "Kelli"
  },
  {
    body: "Ok great!",
    username: "Cody"
  }
])

puts "✅ Done seeding!"
Enter fullscreen mode Exit fullscreen mode

When we run bundle exec rake db:seed, we'll seed our messages table with this data. How cool! Notice we called Message.create() here, even though we didn't define any methods in our Message class. That's all thanks to Active Record. There are a multitude of built-in methods already defined for us so we don't have to worry about initialization or basic method creation for our classes. Hooray!

Connecting a Front-End

In order to connect your front-end for testing, ensure whatever fetch requests you might be using in say a Javascript or React application, have paths set to http://localhost:9292/messages, or whatever your resource is titled, since that's the port we defined as the server default in our Rakefile. You can change that port number there if needed.

Conclusion

I know that was a lot, but hopefully now you can see how all of these pieces fit together to create a functioning Ruby back-end. There are a lot of moving parts with single responsibility, but that ultimately keeps things organized and functioning appropriately. Give it a shot setting up your own server! You've got this! If you get stuck, Stack Overflow is always there to help.

Top comments (0)