APIs are the backbone of modern web applications, but Rails controllers can get unwieldy when you try to support multiple versions, complex parameter validation, or JSON representations that change over time. The Grape gem offers a lightweight, Rack-based DSL for building RESTful APIs in a clean, object-oriented way. Pair it with the grape-entity plugin, and you get Ruby classes that encapsulate your JSON representation, keeping your controllers thin and your business logic organized.

In this post we’ll walk through:

  1. Defining a Grape API in an object-oriented style
  2. Structuring your endpoints with modules and inheritance
  3. Validating parameters and handling errors
  4. Modeling JSON output with Grape::Entity

By the end, you’ll have a solid foundation for scalable, maintainable APIs.

1. Defining a Base API Class

Start by creating a base API class under app/api:

# app/api/base_api.rb
module BaseAPI
  class Root < Grape::API
    version 'v1', using: :path
    format :json
    prefix :api

    # Global error handling
    rescue_from ActiveRecord::RecordNotFound do |e|
      error_response(message: e.message, status: 404)
    end

    mount API::V1::Users
    mount API::V1::Posts
  end
end

Here we:

  • Set version 'v1' so endpoints live under /api/v1/...
  • Use format :json for JSON responses
  • Mount modular endpoint classes (API::V1::Users, API::V1::Posts)

This keeps your entry point thin—just configuration and mounting.

2. Organizing Endpoints in Modules

Under app/api/api/v1, define resources as classes:

# app/api/api/v1/users.rb
module API
  module V1
    class Users < Grape::API
      resource :users do
        desc 'Return list of users'
        get do
          users = User.all
          present users, with: Entities::UserEntity
        end

        desc 'Return a single user'
        params { requires :id, type: Integer, desc: 'User ID' }
        get ':id' do
          user = User.find(params[:id])
          present user, with: Entities::UserEntity
        end

        desc 'Create a user'
        params do
          requires :email, type: String, desc: 'Email address'
          requires :password, type: String, desc: 'Password'
        end
        post do
          user = User.create!(declared(params))
          present user, with: Entities::UserEntity
        end
      end
    end
  end
end

By placing each resource in its own class, you get:

  • Single responsibility: each file handles one resource
  • Clear mount points: mount API::V1::Users

You can reuse shared helpers or concerns with Ruby modules:

# app/api/api/v1/helpers/auth_helpers.rb
module API
  module V1
    module AuthHelpers
      def authenticate!
        error!('401 Unauthorized', 401) unless current_user
      end

      def current_user
        @current_user ||= User.find_by(token: headers['Auth-Token'])
      end
    end
  end
end

# then include in your API class
helpers API::V1::AuthHelpers
before { authenticate! }

3. Parameter Validation and Error Handling

Grape’s built-in params DSL makes validation declarative:

params do
  requires :name,  type: String,  desc: 'Name of the resource'
  optional :age,   type: Integer, desc: 'Age in years', values: 0..150
  requires :email, type: String,  regexp: /@/, desc: 'Valid email'
end
post do
  # If validation fails, Grape returns a 400 with errors in JSON
  Festival.create!(declared(params, include_missing: false))
end

You can also customize error messages or handle exceptions globally in your BaseAPI.

4. Modeling JSON with Grape::Entity

Rather than building hashes in controllers, define Entity classes:

# app/api/entities/user_entity.rb
module Entities
  class UserEntity < Grape::Entity
    expose :id
    expose :email
    expose :created_at, as: :joined_at
    expose :profile, using: Entities::ProfileEntity
    expose :admin?,  as: :is_admin

    private

    def admin?
      object.role == 'admin'
    end
  end

  class ProfileEntity < Grape::Entity
    expose :first_name
    expose :last_name
    expose :bio, if: ->(profile, _) { profile.bio.present? }
  end
end

In your endpoint, you simply:

present user, with: Entities::UserEntity

This approach gives you:

  • Reusability: reuse entities across endpoints
  • Separation of concerns: representation logic lives in one place
  • Nested exposure: easily include associated objects
  • Conditional fields: omit attributes when blank

5. Putting It All Together

Here’s how a request flows:

  1. Rack routes /api/v1/users/123 to BaseAPI::Root
  2. BaseAPI matches GET /users/:id in API::V1::Users
  3. Params DSL validates id is present and an integer
  4. ActiveRecord fetches User.find(123) or raises RecordNotFound
  5. Error handler catches missing records and returns 404
  6. present invokes Entities::UserEntity to serialize JSON

Each layer has a clear responsibility, you can write unit tests for your helper modules, your service objects, and your entities in isolation.

Benefits at a Glance

  • Modularity: break up large controllers into small, focused classes
  • Versioning: side-by-side support for /v1, /v2, etc., by mounting new classes
  • Maintainability: business logic lives in models or service objects; API classes handle only request/response
  • Testability: entities, helpers, and API classes can each be tested in isolation
  • Performance: Grape is lightweight—no extra controller callbacks or view rendering overhead

Conclusion

Grape and grape-entity provide a powerful, object-oriented approach to building JSON APIs in Ruby. By organizing your endpoints into modular classes, validating input declaratively, and encapsulating JSON structure in entity classes, you keep your codebase clean, scalable, and easy to extend. Next time you start an API project—or need to refactor an existing one—give Grape a try for clear separation of concerns and robust versioning support. Happy coding! 🚀