Building an application with Ruby is pretty interesting (at least for me). You can use Rails if you want to do it fast or you can use Sinatra if you’re interested in building a very lightweight REST API application.
This article should help you create a simple Sinatra RESTFUL application with Rack tasks for ActiveRecord and Rake.
Creating a structure
In my project, I use this structure for the application:
.
├── apps
│ └── # your applications here
├── config.ru # Our Rack config
├── db
│ ├── config.yml # ActiveRecord database configuration
│ ├── migrate
│ │ └── # Migrations
│ └── schema.rb
├── environment.db
├── Gemfile
├── models
│ └── # Our ActiveRecord models (Bases) here
└── Rakefile # Our Rake tasks
Moving next!
Gemfile
We need sinatra
, sinatra-contrib
, activerecord
, rake
, rack
and
logger
in our project and slite3
or other driver for database.
source 'https://rubygems.org'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'activerecord'
gem 'sqlite3'
gem 'rake'
gem 'rack'
gem 'logger'
Run bundle
to install the gems.
Rakefile
If you want to use ActiveRecord tasks with Sinatra, you have to configure
Rackfile. It’s not very hard and you don’t have to install any extensions
(like sinatra-activerecord
).
require 'yaml'
require 'logger'
require 'active_record'
include ActiveRecord::Tasks
class Seeder
def initialize(seed)
@seed = seed
end
def load_seed
raise "Seed file '#{@seed}' does not exist" unless File.file? @seed
load @seed_file
end
end
root = File.expand_path '..', __FILE__
DatabaseTasks.env = ENV['ENV'] || 'development'
conf = File.join root, 'db/config.yml'
DatabaseTasks.database_configuration = YAML.load(File.read(conf))
DatabaseTasks.db_dir = File.join root, 'db'
DatabaseTasks.fixtures_path = File.join root, 'test/fixtures'
DatabaseTasks.migrations_paths = [File.join(root, 'db/migrate')]
DatabaseTasks.seed_loader = Seeder.new File.join root, 'db/seeds.rb'
DatabaseTasks.root = root
task :environment do
ActiveRecord::Base.configurations = DatabaseTasks.database_configuration
ActiveRecord::Base.establish_connection DatabaseTasks.env.to_sym
end
load 'active_record/railties/databases.rake'
Now if you run rake -T
, you should get the similar output:
rake db:create # Creates the database from DATABASE_URL or con...
rake db:drop # Drops the database from DATABASE_URL or confi...
rake db:fixtures:load # Load fixtures into the current environment's ...
rake db:migrate # Migrate the database (options: VERSION=x, VER...
rake db:migrate:status # Display status of migrations
rake db:rollback # Rolls the schema back to the previous version...
rake db:schema:cache:clear # Clear a db/schema_cache.dump file
rake db:schema:cache:dump # Create a db/schema_cache.dump file
rake db:schema:dump # Create a db/schema.rb file that is portable a...
rake db:schema:load # Load a schema.rb file into the database
rake db:seed # Load the seed data from db/seeds.rb
rake db:setup # Create the database, load the schema, and ini...
rake db:structure:dump # Dump the database structure to db/structure.sql
rake db:structure:load # Recreate the databases from the structure.sql...
rake db:version # Retrieves the current schema version number
It’s truncated to 80 chars, but if you just see rake db:
- then you’re good.
Now we should add a config file for our ActiveRecord connection.
ActiveRecord config file
It’s placed at db/config.yml
and here’s how it looks:
development:
adapter: sqlite3
database: development.db
pool: 5
timeout: 5000
For better explanation, you should have a look at the official documentation.
Next we should create migrations for our app.
Migrations
In this tutorial, we will create users
table with 001_create_users.rb
migration placed in db/migrate/
.
class CreateUsers < ActiveRecord::Migration
def up
create_table :users do |t|
t.string :username
t.string :password
t.string :email
end
end
def down
drop_table :users
end
end
This code will create the users
table when migrating.
Run rake db:migrate
to execute migrations.
We have prepared everything, so now we’re gonna create an application!
Creating the application
At first, let’s create our Rack file (config.ru
)
require 'sinatra/base'
require 'sinatra/json'
require 'sinatra/reloader'
require 'active_record'
require 'yaml'
require 'json'
class BaseApp < Sinatra::Base
# Register custom extensions
configure :development do
register Sinatra::Reloader
end
# Configure ActiveRecord
env = ENV['ENV'] || 'development'
root = File.expand_path '..', __FILE__
config = YAML.load(File.read(File.join(root, 'db/config.yml')))
ActiveRecord::Base.configurations = config
ActiveRecord::Base.establish_connection env.to_sym
# Create @body variable
before do
if request.content_length.to_i > 0
request.body.rewind
@body = JSON.parse request.body.read, symbolize_names: true
end
end
end
Dir.glob("./models/*.rb").sort.each do |file|
require file
end
Dir.glob("./apps/*.rb").sort.each do |file|
require file
end
map "/" do
run RootApp
end
So, what’s going on there?
- At first, we require our dependencies.
- We create a new instance of
Sinatra::Base
, which contains some settings and ActiveRecord connection initialization which ourapps/
uses. - Next, because we’re creating a RESTFUL API, we should accept request body
and parse it. This is the
before
block, which invokes by Sinatra before the active route. (so if an user call/route
, Sinatra executesbefore
and then the route). -
Dir.glob
loads our models and apps. Models are loading first because Apps depends on them. -
map
is some sort of namespaces, we could add moremap
to run different apps on these urls, so if you addmap '/users/ do ... end
, Rack will run the code defined in the block for/users/*
url.
In the code, map
binds /
to run RootApp. Let’s take a look how RootApp
looks.
Path: apps/root.rb
class RootApp < BaseApp
get "/" do
@user = User.find_by id: @body[:id]
if @user
json response: {
status: :ok,
errors: [],
data: @user,
request: params,
redirect: nil
}
else
json response: {
status: :fail,
errors: [
"User #{params[:id]} does not exist"
],
data: [],
request: params,
redirect: nil
}
end
end
post '/' do
@user = User.create username: @body[:username],
password: @body[:password],
email: @body[:email]
if @user.valid?
json response: {
status: :ok,
errors: [],
data: @user,
request: @body,
redirect: "/#{@user.id}"
}
else
json response: {
status: :fail,
errors: @user.errors,
data: [],
request: @body,
redirect: "/"
}
end
end
end
class RootApp
extends BaseApp
, so we don’t have to re-initialize
ActiveRecord connection and register extensions again.
get
and post
methods are bindings for GET /
and POST /
requests.
get
Accepts JSON body with id
parameter in it and it’s using ActiveRecord
to find the user by ID. If user with this ID does not exist, we response with
JSON contains errors
key, but if it does exist, we just return the data.
post
uses the same ideology as get
for responding, but in this method
we’re creating a new User instance using ActiveRecord::Base class (User
).
Let’s take a look at it!
Path: models/user.rb
class User < ActiveRecord::Base
validates :username, length: {
minimum: 6,
maximum: 35,
}, uniqueness: true
validates :password, format: {
with: /\A[a-zA-Z0-9!@#\$%^&\(\)]+\z/,
message: "only allows a-z, 0-9 and !@#$%^&*()"
}
end
As you can see, there are no new methods defined, but I’ve added some validations:
- Username should have 6-35 characters and be unique.
- Password should contain only a-z, 0-9 and !@#$%^&*().
You can add validation for :email
field as well, but the best
validation for e-mail is sending a mail to it with a confirmation link.
And… we’re done! Now you should able to run your application with
rackup -p 3000
.
- To create an user, send POST request to
localhost:3000/
with credentials. - To view an user, send GET request to
localhost:3000/
with ID of the user.
You could try to send invalid data or non-existent ID to see if it works
(it should give you a JSON response contains errors
key with some data
inside and the empty data
array).
If you have any questions, feel free to ask them in the comments!