Have you ever wanted to create an API without much hassle? And with that in mind, do you think that using Rails for it is an overkill?
Well, I'm going to show you a quick and easy way to create a simple API using Padrino + Rabl.
Padrino is a Ruby web framework built on top of Sinatra that makes it Rails-like, while Rabl (Ruby API Builder Language) is a Ruby templating engine that can generate JSON, XML and other formats with a simple DSL.
We'll start by creating a padrino project
$ padrino g project tasks -d activerecord -b
This will generate a structure similar to Rails
tasks
app
config
db
models
public
Notice that with -d flag we can choose what ORM to use and -b flag is for instructing bundler to install all dependencies.
Next, we will add the rabl gem in the Gemfile and run 'bundle install'.
gem 'rabl'
and then run
$ bundle install
All should be good at this point. Time to create models! we'll be creating two models: Task and User.
$ padrino g model user first_name:string
Which will generate the User activerecord model and migration
apply orms/activerecord
create models/user.rb
create db/migrate/001_create_users.rb
And now the Task model
$ padrino g model task title:string description:string due_time:date user_id:integer
We run migrations this way
$ padrino rake ar:migrate
CreateUsers: migrated (0.0021s)
CreateTasks: migrated (0.0024s)
Both models should be migrated by now. It's time to create associations at model level.
class User < ActiveRecord::Base
validates_presence_of :first_name
has_many :tasks
end
class Task < ActiveRecord::Base
validates_presence_of :title, :user_id
belongs_to :user
end
If we want to test the models we can do it in the Padrino console this way
$ padrino c
=> Loading development console (Padrino v.0.10.7)
=> Loading Application Tasks
irb(main):001:0>
And typing
> User.create first_name: 'Fernando'
…
> Task.create user_id: 1, title: 'Write blog post', description: 'About rabl + padrino', due_date: 1.hour.from_now
We should now have a user with one task. And we are good to create a tasks controller.
$ padrino g controller tasks get:index get:show
create app/controllers/tasks.rb
create app/helpers/tasks_helper.rb
create app/views/tasks
This generated a tasks controller and a tasks_helper, we indicated that we want the controller to have index and show actions via get.
So, finally we will do some work on the API! Firstly we will create an endpoint to get all of the tasks with it's user name. To get to that we need to tell the app that when we get a request to the 'http://localhost:3000/tasks' meaning the index action for tasks we need to fetch the tasks and return them.
app/controllers/tasks.rb
get :index do
@tasks = Task.all
render 'tasks/index'
end
Here we are telling Padrino to render an index template inside of the tasks folder, but we haven't created it yet, so we might as well do it with rabl.
The cool thing about rabl is that it just works out of the box with Padrino.
app/views/tasks/index.rabl
# app/views/tasks/index.rabl
collection @tasks
attributes :id, :title, :description
child(:user) { attributes :name }
So, here we are telling rabl that we are passing a collection through the @tasks instance variable and that we want to present the attributes :id, :title, :description and the user :first_name through the association.
Great, we are ready to test our API. For that, we need to first run it like this:
$ padrino start
=> Padrino/0.10.7 has taken the stage development at http://0.0.0.0:3000
..
Open our browser to this address http://localhost:3000/tasks and we should get this response:
[{"task":{"id":1,"title":"Write blog post","description":"About rabl + padrino","user":{"first_name":"Fernando"}}}]
Nice! just one thing; there are apps/people that don't like including the name of the resource/model into the root of the json because it's really not necessary. That's no problem, we can do it before we load Padrino.
config/boot.rb
Padrino.before_load do
Rabl.configure do |config|
config.include_json_root = false
end
end
If we go again to this address http://localhost:3000/tasks we should be getting...
[{"id":1,"title":"Write blog post","description":"about rabl","due_time":"2012-10-14T21:58:39-07:00","user":{"first_name":"Fernando"}},{"id":2,"title":"Pay bills","description":"Cable and gas","due_time":"2012-10-16T23:16:38-07:00","user":{"first_name":"John"}}]
Pretty easy huh? But what if we want to return if the task is overdue? easy…
app/views/tasks/index.rabl
collection @tasks
attributes :id, :title, :description
child(:user) { attributes :first_name }
node(:on_time) {|task| task.due_time > Time.now}
And this is the returned value
[{"id":1,"title":"Write blog post","description":"about rabl","due_time":"2012-10-14T21:58:39-07:00","on_time":false,"user":{"first_name":"Fernando"}},{"id":2,"title":"Pay bills","description":"Cable and gas","due_time":"2012-10-16T23:16:38-07:00","on_time":true,"user":{"first_name":"John"}}]
What node is doing is that it is creating a node in the response with the name we describe, the value of that node is defined on the block that the node receives.
What if you want to get a specific task? Simple:
app/controllers/tasks.rb
get :show, :with => :id do
@task = Task.find(params[:id].to_i)
render 'tasks/show'
end
We create a get action with the name show in the tasks controller that can receive a parameter named id, and we use its value to search the database for that record. Then we render the show template.
app/views/tasks/show.rabl
object @task
attributes :id, :title, :description
node :errors do |e|
e.errors
end
We will explain later the node errors.
Now if we go in the browser to http://localhost:3000/tasks/show/1 we should be getting.
{"id":1,"title":"Write blog post","description":"about rabl"}
The first thing you can notice is that we no longer have the square brackets meaning that we only fetched one record.
So it's going all pretty nicely, now… what if we want to create records via API? It's also really easy.
app/controllers/task.rb
post :create do
@task = Task.create(params)
render 'tasks/create'
end
We catch the params and we just use it to create the new record, that's it. But we also need a template to render the response.
app/views/tasks/create.rabl
object @task
extends '/tasks/show'
Ok, time to explain the node erros from the template show. When trying to create a record if we don't pass validation or we get another kind of error, ActiveRecord returns us an instance of that intended record, but unsaved, and with an errors attribute that contains an array of errors. It is important to have that node so we can inform the consumer of that endpoint what happened with the request.
Now, how do we test this?
In order to not have to use browser tools for posting to that endpoint and also keep working with Ruby, we will use this awesome gem called Weary, that I will not describe in detail (maybe in another blogpost).
To install Weary into our application we need to add it to the Gemfile and run "bundle install"
gem 'weary'
We create a proxy for the tasks.
lib/task_proxy.rb
class TaskProxy < Weary::Client
get :index, "http://localhost:3000/tasks"
get :show, "http://localhost:3000/tasks/show/{id}" do |resource|
resource.required :id
end
post :create, 'http://localhost:3000/tasks/create/' do |resource|
resource.required :title, :user_id, :due_time
resource.optional :description
end
end
Having this done we can at the same time in another terminal run the Padrino console and type this:
TaskProxy.new.create(title: 'Call mom', due_time: 3.days.from_now, user_id: 2).perform
returning something like this
=> #<Weary::Response:0x007fa712d03f20 @response=#<Rack::Response:0x007fa712d03e30 @status=200, @header={"content-type"=>"text/html;charset=utf-8", "server"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-04-20)", "date"=>"Mon, 15 Oct 2012 15:16:39 GMT", "connection"=>"close", "Content-Length"=>"47"}, @chunked=false, @writer=#<Proc:0x007fa712d02ad0@/Users/fcastellanos/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/rack-1.4.1/lib/rack/response.rb:28 (lambda)>, @block=nil, @length=47, @body=["{\"id\":12,\"title\":\"Call mom\",\"description\":null}"]>, @status=200>
Notice that @body has the response fro the post we just created and that @status is 200.
So, when we hit the browser again at http://localhost:3000/tasks/ we should be seeing three tasks now.
[{"id":1,"title":"Write blog post","description":"about rabl","due_time":"2012-10-14T21:58:39-07:00","on_time":false,"user":{"first_name":"Fernando"}},{"id":2,"title":"Pay bills","description":"Cable and gas","due_time":"2012-10-16T23:16:38-07:00","on_time":true,"user":{"first_name":"John"}},{"id":11,"title":"Call mom","description":null,"due_time":"2012-10-18T08:15:42-07:00","on_time":true,"user":{"first_name":"John"}},{"id":12,"title":"Call mom","description":null,"due_time":"2012-10-18T08:16:39-07:00","on_time":true,"user":{"first_name":"John"}}]
And that's it! we can now create, find, and get a list of tasks. I hope you found this useful.
