The Yumi library we built in the previous jutsu is ready so all we need to do now is start using it. That’s exactly what we are going to do now.
Master Ruby Web APIs
Enjoying what you’re reading? Checkout the book I’m writing about Ruby Web APIs.
The Bloggy series
You can find the full list of jutsus for this series in this jutsu and the jutsu before this one is available there.
Getting the code
You can get the source code to follow this tutorial from GitHub if you didn’t follow the previous jutsu.
Coding time!
Alright, time to start using Yumi inside Bloggy. In this jutsu, we are going to create our presenters that will inherit from Yumi::Base
. The good news is that it’s going to be pretty easy because all the hard work has already been done in Yumi!
1. Updating the application file
First, we need to make a few changes to the application.rb
file.
Here is the list of changes:
- Add the
META_DATA
constant. That’s the hash we will pass to Yumi in our presenters to represent our API meta data. - Require the library files and our future presenters (also create the folder
app/presenters
). - Yumi needs the api url to build the JSON. Let’s add a helper to our Grape API to do that.
Here is the updated code for the application.rb
file.
# application.rb
require 'grape'
require 'mongoid'
Mongoid.load! "config/mongoid.config"
META_DATA = {
name: 'Bloggy',
description: 'A simple blogging API built with Grape.'
}
# Load files from the models and api folders
Dir["#{File.dirname(__FILE__)}/app/models/**/*.rb"].each { |f| require f }
Dir["#{File.dirname(__FILE__)}/app/api/**/*.rb"].each { |f| require f }
Dir["#{File.dirname(__FILE__)}/app/yumi/**/*.rb"].each { |f| require f }
Dir["#{File.dirname(__FILE__)}/app/presenters/**/*.rb"].each {|f| require f}
# Grape API class. We will inherit from it in our future controllers.
module API
class Root < Grape::API
format :json
prefix :api
helpers do
def base_url
"http://#{request.host}:#{request.port}/api/#{version}"
end
end
# Simple endpoint to get the current status of our API.
get :status do
{ status: 'ok' }
end
mount V1::Admin::Posts
mount V1::Comments
mount V1::Posts
end
end
# Mounting the Grape application
DevblastBlog = Rack::Builder.new {
map "/" do
run API::Root
end
}
Since the admin endpoints use a different prefix, we also need to define the base_url
method in the Admin::Posts
controller.
# app/api/v1/admin/posts.rb
module API
module V1
module Admin
class Posts < Grape::API
version 'v1', using: :path, vendor: 'devblast-blog'
namespace :admin do
helpers do
def base_url
"http://#{request.host}:#{request.port}/api/#{version}/admin"
end
end
resources :posts do
# ...
That’s it for the application.rb
, let’s move on to the presenters. It’s going to be pretty fast so don’t get too excited!
2. Adding Presenters
With all the time we spent making Yumi, the presenters should not be hard to implement. And indeed, we just have to create Ruby classes, inherit from Yumi::Base
and call a few methods before having functional JSON API presenters.
2.1 Post
First, the Post
model. I’m not sure if there is anything to say, it’s pretty simple. We are actually just using the class methods we defined in Yumi.
Create the file app/presenters/post.rb
and put the following content in it.
# app/presenters/post.rb
module Presenters
class Post < ::Yumi::Base
meta META_DATA
attributes :slug, :title, :content
has_many :tags, :comments
links :self
end
end
Now if we’d like to generate the JSON for a post, we can just call Presenters::Post.new('my_base_url', post).as_json_api
. But wait! Before doing that, we need to implement the related presenters.
2.2 Comment
Next up, we have the Comment
presenter. Once again, create the file app/presenters/comment.rb
and put the following inside:
# app/presenters/comment.rb
module Presenters
class Comment < ::Yumi::Base
meta META_DATA
attributes :author, :email, :website, :content
links :self
end
end
And here’s how to use it:
Presenters::Comment.new('my_base_url', comment).as_json_api
2.3 Tag
And finally the Tag presenter. Create app/presenters/tag.rb
with the following content:
# app/presenters/tag.rb
module Presenters
class Tag < ::Yumi::Base
meta META_DATA
attributes :slug, :name
links :self
end
end
Just like our other presenters, you can use this presenter with:
Presenters::Tag.new('my_base_url', tag).as_json_api
2.4 Testing the presenters
Let’s do a quick manual test to check that everything is working. Run racksh
to access the application console. We’re going to call the post presenter in there.
If you don’t have one already, create a post in the db:
Post.create(slug: 'something-random', title: 'Something Random', content: 'Whatever')
And then call the presenter with:
Presenters::Post.new('http://example.com/api', Post.last).as_json_api
And you should get a beautifully generated hash that looks like the JSON API specification.
3. Calling our Presenters
We now have functional presenters and the only step missing is to actually use them in our controllers. Let’s fix that right now.
3.1 Posts
We already know how to use our presenters so we can just update our controller actions. To do so, we instantiate the Post
presenter with the base_url
(given by our little helper from earlier) and the current post.
# app/api/v1/posts.rb
module API
module V1
class Posts < Grape::API
version 'v1', using: :path, vendor: 'devblast-blog'
resources :posts do
desc 'Returns all posts'
get '/' do
Presenters::Post.new(base_url, Post.all.ordered).as_json_api
end
desc "Return a specific post"
params do
requires :id, type: String
end
get ':id' do
Presenters::Post.new(base_url, Post.find(params[:id])).as_json_api
end
end
end
end
end
Try it out! Start your server with shotgun config.ru
and access http://localhost:9393/api/v1/posts
to have this beautiful JSON document in front of your eyes!
{
"meta":{
"name":"Bloggy",
"description":"A simple blogging API built with Grape."
},
"data":[
{
"type":"posts",
"id":"56e7932dc43c42041083f280",
"attributes":{
"slug":"something-random",
"title":"Something Random",
"content":"Whatever"
},
"links":{
"self":"http://localhost:9393/api/v1/posts/56e7932dc43c42041083f280"
},
"relationships":{
"tags":{
"data":[
],
"links":{
"self":"http://localhost:9393/api/v1/posts/56e7932dc43c42041083f280/relationships/tags",
"related":"http://localhost:9393/api/v1/posts/56e7932dc43c42041083f280/tags"
}
},
"comments":{
"data":[
],
"links":{
"self":"http://localhost:9393/api/v1/posts/56e7932dc43c42041083f280/relationships/comments",
"related":"http://localhost:9393/api/v1/posts/56e7932dc43c42041083f280/comments"
}
}
}
},
{
"type":"posts",
"id":"56e26050c43c42917eb7c47d",
"attributes":{
"slug":"fu",
"title":"wtf",
"content":null
},
"links":{
"self":"http://localhost:9393/api/v1/posts/56e26050c43c42917eb7c47d"
},
"relationships":{
"tags":{
"data":[
{
"type":"tags",
"id":"56e26d4dc43c42996c2198e5"
}
],
"links":{
"self":"http://localhost:9393/api/v1/posts/56e26050c43c42917eb7c47d/relationships/tags",
"related":"http://localhost:9393/api/v1/posts/56e26050c43c42917eb7c47d/tags"
}
},
"comments":{
"data":[
{
"type":"comments",
"id":"56e26d5bc43c42996c2198e6"
}
],
"links":{
"self":"http://localhost:9393/api/v1/posts/56e26050c43c42917eb7c47d/relationships/comments",
"related":"http://localhost:9393/api/v1/posts/56e26050c43c42917eb7c47d/comments"
}
}
}
}
],
"links":{
"self":"http://localhost:9393/api/v1/posts"
},
"included":[
{
"type":"tags",
"id":"56e26d4dc43c42996c2198e5",
"attributes":{
"slug":"allo",
"name":"yeah"
},
"links":{
"self":"http://localhost:9393/api/v1/tags/56e26d4dc43c42996c2198e5"
}
},
{
"type":"comments",
"id":"56e26d5bc43c42996c2198e6",
"attributes":{
"author":"tibo",
"email":null,
"website":null,
"content":"fu"
},
"links":{
"self":"http://localhost:9393/api/v1/comments/56e26d5bc43c42996c2198e6"
}
}
]
}
3.2 Comments
Let’s also update the comment endpoints with the Comment
presenter. We only updated the create
and update
actions by returning the hash generated by as_json_api
.
# app/api/v1/comments.rb
module API
module V1
class Comments < Grape::API
version 'v1', using: :path, vendor: 'devblast-blog'
# Nested resource so we need to add the post namespace
namespace 'posts/:post_id' do
resources :comments do
desc 'Create a comment.'
params do
requires :author, type: String
requires :email, type: String
requires :website, type: String
requires :content, type: String
end
post do
post = Post.find(params[:post_id])
comment = post.comments.create!({
author: params[:author],
email: params[:email],
website: params[:website],
content: params[:content]
})
Presenters::Comment.new(base_url, comment).as_json_api
end
desc 'Update a comment.'
params do
requires :id, type: String
requires :author, type: String
requires :email, type: String
requires :website, type: String
requires :content, type: String
end
put ':id' do
post = Post.find(params[:post_id])
comment = post.comments.find(params[:id])
comment.update!({
author: params[:author],
email: params[:email],
website: params[:website],
content: params[:content]
})
Presenters::Comment.new(base_url, comment.reload).as_json_api
end
desc 'Delete a comment.'
params do
requires :id, type: String, desc: 'Status ID.'
end
delete ':id' do
post = Post.find(params[:post_id])
post.comments.find(params[:id]).destroy
end
end
end
end
end
end
And that’s it! We don’t have any controller for Tags
so we can skip that.
Note that in our JSON, related and included resources have a related
and/or self
links pointing to endpoints that we don’t have for comments and tags. To follow the JSON API specification, we should actually have a controller for tags and more actions for comments in order to access the individual resources and the relationships. You can add them as an exercise ;)
4. Manual Testing
I’m getting bored of having to manually test everything…if only there was a way to automate all that :troll:.
Of course there is a way and we’re going to do it very soon! But for now, here are a few cURL requests you can run to test your endpoints.
Client Posts controller cURL Requests
Start your server with shotgun config.ru
to run the following queries.
- Get the list of posts
curl http://localhost:9393/api/v1/posts
- Get a specific post (replace the
POST_ID
part)
curl http://localhost:9393/api/v1/posts/POST_ID
Client Comments controller cURL Requests
- Create a comment (change the
POST_ID
)
curl http://localhost:9393/api/v1/posts/POST_ID/comments -d "author=thibault&email=thibault@example.com&website=devblast.com&content=Cool"
- Update a comment (change the
POST_ID
andCOMMENT_ID
)
curl -X PUT http://localhost:9393/api/v1/posts/POST_ID/comments/COMMENT_ID -d "author=tibo&email=tibo@example.com&website=example.com&content=Awesome"
- Delete a comment (change the
POST_ID
andCOMMENT_ID
)
curl -X DELETE http://localhost:9393/api/v1/posts/POST_ID/comments/COMMENT_ID
The End
We’re pretty much done with our JSON API implementation. We skipped a few things to be honest because it was just too much. I hope that what we saw motivated you to try out JSON API by yourself and find out more about what the specification has to offer.
Before wrapping up the API and starting the series about the front-end, we need to add two more things. Currently, the request payload that we handle are not compliant with the JSON API specification. Let’s change that and ensure we only allow one media type (application/vnd.api+json
) in Bloggy. After that, we will add automated tests in order to avoid regression and be able to sleep at night easily.
Source Code
The source code is available on GitHub.