Project

respondo

0.0
The project is in a healthy, maintained state
Respondo standardizes JSON API responses across Rails applications. Every response gets success, data, message, and meta fields. Automatic pagination meta for Kaminari and Pagy collections. ActiveRecord serialization, error extraction, and flexible HTTP codes built in.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.12
~> 0.22
~> 8.1.3
 Project Readme

Respondo ๐ŸŽฏ

Consistent JSON API responses for Rails โ€” in one line.

Gem Version Downloads License: MIT Ruby


The problem every Rails API developer hits

You're building an API consumed by a React frontend and a Flutter app. Three developers on your team. Three different response shapes:

// Developer A
{ "data": [...], "status": "ok" }

// Developer B
{ "result": [...], "success": true }

// Developer C
{ "users": [...] }

Your frontend devs are writing if (res.data || res.result || res.users). Your Flutter devs are filing bugs. Your code reviews are arguments.

Respondo fixes this permanently. One response shape. Every controller. Every developer. Every time.

{
  "success": true,
  "message": "Users fetched",
  "data": [...],
  "meta": {
    "timestamp": "2024-06-15T10:30:00Z",
    "pagination": { "currentPage": 1, "totalPages": 4, "totalCount": 98 }
  }
}

Install

# Gemfile
gem "respondo"
bundle install
rails generate respondo:install

That's it. No include in ApplicationController. No boilerplate. Respondo auto-injects via Railtie.


Your first response (30 seconds)

class UsersController < ApplicationController
  def index
    render_ok(data: User.all, message: "Users fetched")
  end

  def create
    user = User.new(user_params)
    if user.save
      render_created(data: user, message: "Account created")
    else
      render_unprocessable(message: "Validation failed", errors: user.errors)
    end
  end

  def show
    render_ok(data: User.find(params[:id]), message: "User found")
  rescue ActiveRecord::RecordNotFound
    render_not_found(message: "User not found")
  end
end

Your frontend now gets a guaranteed structure โ€” forever.


Why teams switch to Respondo

Without Respondo With Respondo
Each dev invents their own response format One standard, enforced automatically
Frontend code full of defensive `
Pagination shape differs per endpoint Pagination always in meta.pagination
Validation errors in different keys Always in errors, always a hash
render json: boilerplate in every action One expressive method call
camelCase conversion scattered across code config.camelize_keys = true โ€” done

What every response looks like

Every response โ€” success or error โ€” has the same four keys:

Key Type Description
success Boolean true or false โ€” always present
message String Human-readable description
data Object / Array / nil The payload
meta Object Timestamp + pagination + optional request_id

Error responses additionally include errors โ€” a hash of { field: ["message"] }.

Success:

{
  "success": true,
  "message": "Post published",
  "data": { "id": 42, "title": "Hello World" },
  "meta": { "timestamp": "2024-06-15T10:30:00Z" }
}

Validation error:

{
  "success": false,
  "message": "Validation failed",
  "data": null,
  "errors": { "email": ["is invalid"], "name": ["can't be blank"] },
  "meta": { "timestamp": "2024-06-15T10:30:00Z" }
}

Real-world controller (with pagination)

class PostsController < ApplicationController

  def index
    @posts = Post.published.page(params[:page]).per(params[:per_page] || 20)

    render_ok(
      data:    @posts,
      message: "Posts fetched",
      pagination: {
        current_page: @posts.current_page,
        next_page:    @posts.next_page,
        prev_page:    @posts.prev_page,
        total_pages:  @posts.total_pages,
        total_count:  @posts.total_count,
        per_page:     @posts.limit_value
      }
    )
  end

  def create
    @post = current_user.posts.build(post_params)

    if @post.save
      render_created(data: @post, message: "Post published")
    else
      render_unprocessable(message: "Could not create post", errors: @post.errors)
    end
  end

  def update
    @post = Post.find(params[:id])

    return render_forbidden(message: "Not your post") unless @post.user == current_user

    if @post.update(post_params)
      render_ok(data: @post, message: "Post updated")
    else
      render_unprocessable(message: "Update failed", errors: @post.errors)
    end
  rescue ActiveRecord::RecordNotFound
    render_not_found(message: "Post not found")
  end

end

Auto-Serialization

Respondo automatically handles:

Input type Output
ActiveRecord::Base instance record.as_json
ActiveRecord::Relation Array of as_json records
ActiveModel::Errors { field: ["message", ...] }
Hash Passed through (values serialized)
Array Each element serialized recursively
Exception { message: e.message }
Anything with #as_json .as_json
Anything with #to_h .to_h
Primitives (String, Integer...) As-is

Configuration

Run the interactive generator โ€” it walks you through every option:

rails generate respondo:install

Or write it manually:

# config/initializers/respondo.rb
Respondo.configure do |config|
  config.default_success_message = "OK"
  config.default_error_message   = "Something went wrong"
  config.include_request_id      = true   # adds request_id to every meta
  config.camelize_keys           = true   # snake_case โ†’ camelCase (Flutter/JS friendly)
  config.default_meta            = { api_version: "v1" }
end

camelCase output (great for Flutter / React Native)

config.camelize_keys = true
{
  "success": true,
  "data": { "firstName": "Alice", "createdAt": "2024-01-01" },
  "meta": { "totalPages": 4, "currentPage": 1 }
}

Custom serializer

config.serializer = ->(obj) { MySerializer.new(obj).as_json }

Global error handling (recommended pattern)

Add this to ApplicationController to handle exceptions app-wide without try/rescue in every action:

class ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound do |e|
    render_not_found(message: e.message)
  end

  rescue_from ActionController::ParameterMissing do |e|
    render_bad_request(message: e.message)
  end

  rescue_from StandardError do |e|
    Rails.logger.error(e.full_message)
    render_server_error(message: "An unexpected error occurred")
  end
end

Complete HTTP helper reference

Respondo covers every HTTP status code. Here are the helpers you'll use every day:

2xx โ€” Success

Helper Status When to use
render_ok 200 Standard success
render_created 201 After POST creates a resource
render_accepted 202 Async jobs โ€” request queued
render_no_content 200* After DELETE โ€” no body

*Rails renders 200 with a JSON body for render_no_content to preserve consistent structure.

render_ok(data: @user, message: "Profile fetched")
render_created(data: @order, message: "Order placed")
render_accepted(data: { job_id: "abc123" }, message: "Export queued โ€” you'll get an email")
render_no_content(message: "Account deleted")

4xx โ€” Client errors

Helper Status When to use
render_bad_request 400 Malformed input
render_unauthorized 401 Not logged in / token expired
render_forbidden 403 Logged in but not allowed
render_not_found 404 Record doesn't exist
render_conflict 409 Duplicate (e.g. email taken)
render_unprocessable 422 Validation errors
render_too_many_requests 429 Rate limiting
render_unauthorized(message: "Token has expired", errors: { token: ["has expired"] })
render_forbidden(message: "You can only edit your own posts")
render_not_found(message: "User ##{params[:id]} not found")
render_unprocessable(message: "Validation failed", errors: user.errors)
render_conflict(message: "Email already registered", errors: { email: ["has already been taken"] })
render_too_many_requests(message: "Slow down โ€” 100 req/min max", meta: { retry_after: 60 })

5xx โ€” Server errors

render_server_error(message: "Something went wrong. Our team has been notified.")
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.", meta: { retry_after: 1800 })
render_bad_gateway(message: "Payment processor is unreachable โ€” you have not been charged")
Full list of all helpers (click to expand)

1xx โ€” Informational

render_continue ยท render_switching_protocols ยท render_processing ยท render_early_hints

2xx โ€” Success

render_ok ยท render_created ยท render_accepted ยท render_non_authoritative ยท render_no_content ยท render_reset_content ยท render_partial_content ยท render_multi_status ยท render_already_reported ยท render_im_used

3xx โ€” Redirect

render_multiple_choices ยท render_moved_permanently ยท render_found ยท render_see_other ยท render_not_modified ยท render_temporary_redirect ยท render_permanent_redirect

4xx โ€” Client Error

render_bad_request ยท render_unauthorized ยท render_payment_required ยท render_forbidden ยท render_not_found ยท render_method_not_allowed ยท render_not_acceptable ยท render_proxy_auth_required ยท render_request_timeout ยท render_conflict ยท render_gone ยท render_length_required ยท render_precondition_failed ยท render_payload_too_large ยท render_uri_too_long ยท render_unsupported_media_type ยท render_range_not_satisfiable ยท render_expectation_failed ยท render_im_a_teapot ยท render_misdirected_request ยท render_unprocessable ยท render_locked ยท render_failed_dependency ยท render_too_early ยท render_upgrade_required ยท render_precondition_required ยท render_too_many_requests ยท render_request_header_fields_too_large ยท render_unavailable_for_legal_reasons

5xx โ€” Server Error

render_server_error ยท render_not_implemented ยท render_bad_gateway ยท render_service_unavailable ยท render_gateway_timeout ยท render_http_version_not_supported ยท render_variant_also_negotiates ยท render_insufficient_storage ยท render_loop_detected ยท render_not_extended ยท render_network_authentication_required


Testing your responses

Respondo ships with test helpers for RSpec and Minitest so you can assert on response structure directly.

RSpec

# spec/rails_helper.rb
require "respondo/testing/rspec"

RSpec.describe UsersController, type: :request do
  describe "GET /users" do
    it "returns a success response" do
      get "/users"
      expect(response).to be_respondo_success
      expect(response).to have_respondo_message("Users fetched")
    end
  end

  describe "POST /users with invalid params" do
    it "returns validation errors" do
      post "/users", params: { user: { email: "" } }
      expect(response).to be_respondo_error
      expect(response).to have_respondo_errors(:email)
    end
  end
end

Minitest

# test/test_helper.rb
require "respondo/testing/minitest"

class UsersControllerTest < ActionDispatch::IntegrationTest
  def test_index_returns_success
    get users_url
    assert_respondo_success response
    assert_respondo_message "Users fetched", response
  end
end

What's next

  • ActiveModelSerializers / Blueprinter auto-integration
  • OpenAPI / Swagger schema generation from Respondo helpers
  • Rack middleware for zero-config global exception handling

Have a feature idea? Open an issue โ†’


Contributing

Bug reports and pull requests are welcome on GitHub.

git clone https://github.com/spatelpatidar/respondo
cd respondo
bundle install
bundle exec rspec

License

Released under the MIT License.


If Respondo saved you an hour, give it a โญ โ€” it helps other developers find it.