Back to Tutorials
Jutsu6/8/2025

Bloggy #4 - Integrating Yumi, our JSON API implementation, in Bloggy

grapejson apirubyyumi
Bloggy #4 - Integrating Yumi, our JSON API implementation, in Bloggy

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:

  1. Add the META_DATA constant. That's the hash we will pass to Yumi in our presenters to represent our API meta data.
  2. Require the library files and our future presenters (also create the folder app/presenters).
  3. 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=devmystify.com&content=Cool"
  • Update a comment (change the POST_ID and COMMENT_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 and COMMENT_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.

Comments

Loading comments...

Level Up Your Dev Skills & Income 💰💻

Learn how to sharpen your programming skills, monetize your expertise, and build a future-proof career — through freelancing, SaaS, digital products, or high-paying jobs.

Join 3,000+ developers learning how to earn more, improve their skills, and future-proof their careers.

Bloggy #4 - Integrating Yumi, our JSON API implementation, in Bloggy | Devmystify