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:
- Defining a Grape API in an object-oriented style
- Structuring your endpoints with modules and inheritance
- Validating parameters and handling errors
- 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:
- Rack routes
/api/v1/users/123
toBaseAPI::Root
- BaseAPI matches
GET /users/:id
inAPI::V1::Users
- Params DSL validates
id
is present and an integer - ActiveRecord fetches
User.find(123)
or raisesRecordNotFound
- Error handler catches missing records and returns 404
- 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! 🚀