Skip to Content
Course content

123: Building a JSON API with Rails

Click on the "Edit" button in the top corner of the screen to edit your slide content.

So, you've got your Rails app running, your models are set up, and you're comfortable with the standard MVC flow. But lately, most of the apps we build aren't just serving HTML pages—they're serving data to a frontend framework like React or a mobile app. We need a JSON API.

I've already got a Book model in my app with title, isbn, and summary. Let's see what happens when we try to expose this to the world without overthinking it.

Just throwing data at the screen

My first instinct is usually to just create a controller and tell Rails to render the object as JSON. I'll set up a BooksController with an index action:

class BooksController < ApplicationController
  def index
    @books = Book.all
    render json: @books
  end
end

I'll hit that endpoint in my browser or via Curl, and it works! It returns an array of book objects. But as I look at the output, I notice a problem. Rails is dumping everything. I'm seeing id, created_at, and updated_at. My API consumer doesn't need to know exactly what millisecond a record was updated in my database; that's internal noise.

Trimming the fat

I want to be explicit about what I'm sending. I remember that render json: actually calls as_json under the hood. I'll try to pass some options to that call right in the controller to see if I can filter the fields.

def index
  @books = Book.all
  render json: @books.as_json(only: [:title, :isbn])
end

That's better. The timestamps are gone. But here's where it gets annoying: what if I want to include the author? I have a Book belongs_to :author relationship. If I just add :author to the only list, Rails will just give me the author's ID or a weird string, not the actual author object.

Bringing in the Author

I need to nest the data. Let's try using the include option. I'll tell Rails to include the author, but I probably don't want the author's password hash or email address in a public book list.

def index
  @books = Book.all
  render json: @books.as_json(
    only: [:title, :isbn],
    include: { author: { only: [:name, :bio] } }
  )
end

Now the output looks like a professional API response. However, looking at this controller, I'm starting to cringe. If I have five different actions (show, index, update, etc.) and I'm manually defining these only and include hashes everywhere, I'm going to have a maintenance nightmare. If I rename bio to biography in the Author model, I have to hunt down every single JSON hash in my controllers.

Moving the logic out of the controller

This is the "aha!" moment where I realize the controller shouldn't be responsible for the shape of the JSON. We need a presentation layer. While there are gems like ActiveModelSerializers or Panko, for a lot of projects, a simple Jbuilder template (which comes with Rails) is the cleanest way to handle this. It lets us write Ruby code to build the JSON structure in a separate file.

I'll remove the render json: line from my controller and just let it look for a view. Then I'll create app/views/books/index.json.jbuilder:

json.array! @books do |book|
  json.title book.title
  json.isbn  book.isbn
  json.author do
    json.name book.author.name
    json.bio  book.author.bio
  end
end

I just moved the "how it looks" logic out of the "how it works" logic. Now, if I want to add a virtual field—like a full_description that combines the summary and the author's bio—I can just do it right here in the Jbuilder file without cluttering my controller. This feels much more scalable.




📋 Practical Task

Building a Public Book Search Endpoint

You have a Book model and a Genre model (Book belongs_to Genre). Create a SearchController with a results action that takes a query parameter q to search for books by title.

Your task is to implement this using a Jbuilder template. The JSON response for each book must include:

  • The book's title.
  • The book's publication_year.
  • A nested genre object containing only the genre's name.
  • A custom field called search_term that echoes back the query parameter q used for the search.

Ensure your controller does not contain any as_json or only: [...] logic; all formatting must happen in the .json.jbuilder file.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.