This is a step by step guide on how I setup a Grape API on Rack.
Our API will have a few dependencies:
These are all gems that most people are familiar with, hopefully. Click on the links to find out more.
First lets create our project folder: mkdir grape_app
Create a Gemfile
with the dependencies listed above. It should looks like this:
source 'https://rubygems.org'
gem 'grape'
gem 'grape-swagger'
gem 'rack-cors'
gem 'activerecord'
gem 'sqlite3'
Run bundle install
to install the dependencies which will give us everything we need to create our application.
To have our API accepting HTTP requests we are going to mount it on top of Rack. Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks.
Create a config.ru
file and make it look like this:
# config.ru
require 'rack'
require 'grape' # require the Grape library
class API < Grape::API
get :hello do
{hello: "world"}
end
end
run API # Mounts on top of Rack.
A little explanation is need at this point. Our API class is our API essentially, it does nothing more than respond to a GET request and return an XML response that looks like this:
<hash>
<hello>world</hello>
</hash>
Once we have this file created and you run rackup
you can visit http://localhost:9292/hello you'll see the above response.
There we have it, a very naive API mounted on top of Rack. Simple enough so far, right? Let's keep going and make this more professional.
What good is an API without a DB and ORM? I'm quite familiar with ActiveRecord from working with Rails a lot, so I'm going to use this as our API's ORM. At the moment I'm not concerned with running our API in different environments so I'm just going to use sqlite as our DB for convenience.
Rails is great because it makes working with the DB extremely convenient, from it's rake tasks to migrations. I want to be able to have these conveniences in our Grape API.
ActiveRecord contains a module called DatabaseTasks
for the drop, create, and migrate functions that we use as rake tasks. This module needs to know, what our current environment is (development or production), our db dir(the folder that will contains all the migrations and schema.rb), where the DB configuration file is (our database.yml), and our migrations folder (db/migrate).
Let's create our folder structure before adding the tasks to our project. Run: mkdir -p db/migrate config
Add our database configuration, just like you normally would when working with a Rails project. Create a database.yml
file in config/
that looks like this:
development:
adapter: sqlite3
database: development.sqlite3
To use the the drop, create, and migration rake tasks we need to create a Rakefile.rb. This file will gather all the environment and project information the DatabaseTask
module requires to run the DB functions, and include our rake tasks to execute them.
Create a Rakefile.rb
and copy the code below into it.
require 'bundler/setup'
require 'active_record'
## Start tasks setup.
# The next few lines of code will allow us to use ActiveRecord tasks like
# db:migrate, etc. outside of rails.
include ActiveRecord::Tasks
db_dir = File.expand_path('../db', __FILE__)
config_dir = File.expand_path('../config', __FILE__)
DatabaseTasks.env = ENV['RACK_ENV'] || 'development'
DatabaseTasks.db_dir = db_dir
DatabaseTasks.database_configuration = YAML.load(File.read(File.join(config_dir, 'database.yml')))
DatabaseTasks.migrations_paths = File.join(db_dir, 'migrate')
task :environment do
ActiveRecord::Base.configurations = DatabaseTasks.database_configuration
ActiveRecord::Base.establish_connection DatabaseTasks.env
end
load 'active_record/railties/databases.rake'
## End tasts setup.
Try and run rake db:create
and you will encounter error that says uninitialized constant ActiveRecord::Tasks::DatabaseTasks::Rails
. This is because we are using sqlite. Thankfully there is a fix courtesy of the comments here https://gist.github.com/drogus/6087979. This is also where I found the code that allows us to use the DB rake tasks we are used to in Rails.
By adding a few extra lines to our Rakefile.rb
we can work around this problem. These lines are:
module Rails
def self.root
File.dirname(__FILE__)
end
def self.env
ENV['RACK_ENV'] || 'development'
end
end
Your Rakefile.rb
should look something like this now:
require 'bundler/setup'
require 'active_record'
## Start tasks setup.
# The next few lines of code will allow us to use ActiveRecord tasks like
# db:migrate, etc. outside of rails.
# Redefining the Rails module and overriding the root and env methods to fix the
# errors mentioned here: https://gist.github.com/drogus/6087979
module Rails
def self.root
File.dirname(__FILE__)
end
def self.env
ENV['RACK_ENV'] || 'development'
end
end
include ActiveRecord::Tasks
db_dir = File.expand_path('../db', __FILE__)
config_dir = File.expand_path('../config', __FILE__)
DatabaseTasks.env = ENV['RACK_ENV'] || 'development'
DatabaseTasks.db_dir = db_dir
DatabaseTasks.database_configuration = YAML.load(File.read(File.join(config_dir, 'database.yml')))
DatabaseTasks.migrations_paths = File.join(db_dir, 'migrate')
task :environment do
ActiveRecord::Base.configurations = DatabaseTasks.database_configuration
ActiveRecord::Base.establish_connection DatabaseTasks.env
end
load 'active_record/railties/databases.rake'
## End tasts setup.
At this stage all we can really do is create and drop a DB. How do we add migrations?
There are two gems that I've found that can achieve this but neither of them seemed to work the way I wanted them to. So to work around this I wrote my own rake task to generate a migration.
Which looks exactly like this:
namespace :db do
desc "Generates a migration file in db/migrate/"
task :migration, :name do |t, args|
File.open("db/migrate/#{Time.now.getutc.strftime('%Y%m%d%H%M%S')}_#{args[:name]}.rb", 'w') do |f|
f.write("class #{args[:name].classify} < ActiveRecord::Migration
def up
end
def down
end
end")
end
end
end
Just add that to your Rakefile.rb
and away you go!
Finally we are ready to add ActiveRecord to our API. All we need to do is require the active_record
library, set up the logger, and create the connection to the DB
Change your config.ru to looks like this:
# config.ru
require 'rack'
require 'grape'
require 'active_record'
# Enable ActiveRecord logging.
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
# DB Connection
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'development.sqlite3')
class API < Grape::API
get :hello do
{hello: "world"}
end
end
run API
As you can see we added three new lines. The first line requires the ActiveRecord libraray, the second sets up our ORM logging, and the third uses ActiveRecord to establish a connection to our development DB.
If you wanted you could, create a migration and define a model inside the config.ru and happily work away but this is not very maintainable. We need to add some structure to our project.
.
├── Gemfile
├── Gemfile.lock
├── Rakefile.rb
├── app
│ ├── api.rb
│ ├── models
│ │ └── user.rb
│ ├── models.rb
│ ├── mutations
│ ├── routes
│ │ └── user_routes.rb
│ └── routes.rb
├── config
│ └── database.yml
├── config.ru
├── db
│ ├── migrate
│ │ └── 20150718184431_initial_migration.rb
│ └── schema.rb
└── development.sqlite3