Respondo 🎯

Gem Version GitHub Repo Views

Smart JSON API response formatter for Rails — consistent structure every time, across every app.

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

The Problem

Different developers return different JSON structures — some use data, some use result, some forget success: true. This makes frontend integration (Flutter, React, etc.) brittle and unpredictable.

Respondo enforces one structure, everywhere, automatically.


Installation

gem "respondo"

Setup

After adding the gem, run:

rails generate respondo:install

The interactive wizard walks you through every option and writes a fully commented config/initializers/respondo.rb tailored to your project. No need to read the full README or copy-paste config by hand.

  ┌─ Project Info ─────────────────────────────────────────────────────┐

  Project / app name
  (Used as a comment header in the initializer)
  › [MyApp]:

  API version  (e.g. v1 — added to every response meta block)
  › [v1]:

  ┌─ Response Messages ────────────────────────────────────────────────┐

  Default success message
  › [Success]:

  Default error message
  › [An error occurred]:

  ...

The generator produces a file like this:

# frozen_string_literal: true

# Respondo initializer — MyApp
# Generated by: rails generate respondo:install
# Respondo version: 2.1.0

Respondo.configure do |config|

  # ── Messages ─────────────────────────────────────────────────────────
  config.default_success_message = "Success"
  config.default_error_message   = "Something went wrong"

  # ── Request ID ───────────────────────────────────────────────────────
  config.include_request_id = true

  # ── Key Format ───────────────────────────────────────────────────────
  config.camelize_keys = true

  # ── Global Meta ──────────────────────────────────────────────────────
  config.default_meta = {
    api_version: "v1",
    platform: "mobile"
  }

  # ── Custom Serializer ────────────────────────────────────────────────
  # config.serializer = ->(obj) { MySerializer.new(obj).as_json }

end

Re-run the generator any time to regenerate with different answers — it overwrites the existing initializer.


Manual setup (alternative)

If you prefer to write the initializer yourself, create config/initializers/respondo.rb:

# 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 for Flutter/JS
end

Respondo auto-includes itself into ActionController::Base and ActionController::API via Railtie. No manual include needed in ApplicationController.


Response Structure

Every single response — success or error — returns the same four keys:

Key Type Description
success Boolean true or false
message String Human-readable description
data Object/Array/nil The payload
meta Object Timestamp + pagination (if passed) + optional request_id

Error responses additionally include:

Key Type Description
errors Hash Field-level errors { field: ["message", ...] }

errors is optional. Pass it when you want to surface field-level detail to the client (e.g. form validation, token issues). Omit it for simple human-readable-only responses — message alone is perfectly valid.


Complete Helper Reference

1xx — Informational Helpers

1xx responses are protocol-level and carry no body in standard HTTP/1.1. Respondo still returns a JSON body for API logging consistency. Use with care.

render_continue — 100

render_continue
render_continue(message: "Continue sending request body")

render_switching_protocols — 101

render_switching_protocols
render_switching_protocols(message: "Upgrading to WebSocket")

render_processing — 102

render_processing
render_processing(message: "Working on it — please wait")

render_early_hints — 103

render_early_hints
render_early_hints(message: "Early hints provided", meta: { link: "</style.css>; rel=preload" })

2xx — Success Helpers

render_ok — 200 OK

Explicit alias for render_success. Use when you want to be more descriptive.

render_ok(data: @user, message: "User found")

render_created — 201 Created

Use after a successful POST that creates a resource.

render_created(data: @post, message: "Post published")
render_created(data: @user)  # uses default "Created successfully" message

render_accepted — 202 Accepted

Use for async operations — the request was received but processing happens in the background.

render_accepted(message: "Your export is being processed. You will receive an email when ready.")
render_accepted(data: { job_id: "abc123" }, message: "Job queued")

render_non_authoritative — 203 Non-Authoritative Information

Use when data comes from a third-party or cache rather than the origin server.

render_non_authoritative(data: @user, message: "Data sourced from cache")

render_no_content — 200 OK

Use after DELETE or actions with no meaningful response body. Returns standard JSON structure for consistency.

render_no_content                                  # "Deleted successfully"
render_no_content(message: "Account deactivated")

render_reset_content — 205 Reset Content

Tell the client to reset the document view (e.g. clear a form after submission).

render_reset_content(message: "Form submitted — please reset the view")

render_partial_content — 206 Partial Content

Use for chunked or range-based responses.

render_partial_content(data: @chunk, message: "Page 1 of 5")
render_partial_content(data: @results, meta: { range: "0-99/500" })

render_multi_status — 207 Multi-Status

Use for batch operations where some succeed and some fail.

render_multi_status(
  data: { created: 8, failed: 2 },
  message: "Batch completed with partial failures"
)

render_already_reported — 208 Already Reported

render_already_reported(data: @resource, message: "Already reported in this binding")

render_im_used — 226 IM Used

render_im_used(data: @resource, message: "Delta encoding applied")

3xx — Redirect Helpers

These return a JSON body so your API can communicate redirect intent with context. Pass the target URL via meta: { redirect_url: "..." }.

render_multiple_choices — 300

render_multiple_choices(
  data: [{ format: "json", url: "/resource.json" }, { format: "xml", url: "/resource.xml" }],
  message: "Multiple representations available"
)

render_moved_permanently — 301

render_moved_permanently(message: "This endpoint has moved", meta: { redirect_url: new_url })

render_found — 302

render_found(message: "Resource temporarily at another URL", meta: { redirect_url: temp_url })

render_see_other — 303

render_see_other(message: "See the canonical resource", meta: { redirect_url: canonical_url })

render_not_modified — 304

render_not_modified(message: "Your cached version is still valid")

render_temporary_redirect — 307

render_temporary_redirect(message: "Repeat this request at the redirect URL", meta: { redirect_url: url })

render_permanent_redirect — 308

render_permanent_redirect(message: "Permanently moved — update your bookmarks", meta: { redirect_url: url })

4xx — Client Error Helpers

Two usage patterns for every error helper:

  • Message only — a human-readable string shown to the end user.
  • Message + errors — add errors: when you also need field-level detail for the client to act on (e.g. highlight a form field, log a specific token issue). errors is a Hash of { field: ["message", ...] }.

render_bad_request — 400 Bad Request

# Message only
render_bad_request(message: "The 'date' parameter is required")

# Message + errors
render_bad_request(message: "Invalid input", errors: { date: "must be a valid date"})

render_unauthorized — 401 Unauthorized

# Message only
render_unauthorized(message: "Please log in to continue")

# Message + errors
render_unauthorized(message: "Token has expired", errors: { token: "has expired, please log in again"})

render_payment_required — 402 Payment Required

# Message only
render_payment_required(message: "Upgrade to Pro to access this feature")

# Message + errors
render_payment_required( message: "Upgrade to Pro to access this feature", errors: { plan: "must be Pro or higher to access this feature"})

render_forbidden — 403 Forbidden

# Message only
render_forbidden(message: "You can only edit your own posts")

# Message + errors
render_forbidden(message: "You can only edit your own posts",errors: { post: "does not belong to you" })

render_not_found — 404 Not Found

# Message only
render_not_found(message: "User not found")

# Message + errors
render_not_found(message: "User not found", errors: { id: "no user exists with this ID" })

render_method_not_allowed — 405

# Message only
render_method_not_allowed(message: "This endpoint only accepts POST requests")

# Message + errors
render_method_not_allowed(message: "This endpoint only accepts POST requests",errors: { method: "GET is not allowed, use POST" })

render_not_acceptable — 406

# Message only
render_not_acceptable(message: "Only application/json is supported")

# Message + errors
render_not_acceptable(message: "Only application/json is supported", errors: { content_type: "must be application/json" })

render_proxy_auth_required — 407

# Message only
render_proxy_auth_required(message: "Authenticate with the proxy first")

# Message + errors
render_proxy_auth_required(message: "Authenticate with the proxy first", errors: { proxy_token: "is missing or invalid"})

render_request_timeout — 408

# Message only
render_request_timeout(message: "The query took too long. Try a smaller date range.")

# Message + errors
render_request_timeout( message: "The query took too long. Try a smaller date range.", errors: { date_range: "must span 90 days or fewer" })

render_conflict — 409 Conflict

# Message only
render_conflict(message: "Email address is already registered")

# Message + errors
render_conflict(message: "Email address is already registered",errors: { email: "has already been taken" })

render_gone — 410 Gone

# Message only
render_gone(message: "This account has been permanently deleted")

# Message + errors
render_gone(message: "This account has been permanently deleted",errors: { account: ["no longer exists and cannot be recovered"] })

render_length_required — 411

# Message only
render_length_required(message: "Content-Length header is required")

# Message + errors
render_length_required(message: "Content-Length header is required",errors: { content_length: ["header is missing from the request"] })

render_precondition_failed — 412

# Message only
render_precondition_failed(message: "Resource has been modified since your last request")

# Message + errors
render_precondition_failed(message: "Resource has been modified since your last request",errors: { etag: ["does not match the current resource version"] })

render_payload_too_large — 413

# Message only
render_payload_too_large(message: "File exceeds the 10 MB upload limit")

# Message + errors
render_payload_too_large(message: "File exceeds the 10 MB upload limit",errors: { file: ["must be smaller than 10 MB"] })

render_uri_too_long — 414

# Message only
render_uri_too_long(message: "That URL is too long to process")

# Message + errors
render_uri_too_long(message: "That URL is too long to process",errors: { url: ["must not exceed 2048 characters"] })

render_unsupported_media_type — 415

# Message only
render_unsupported_media_type(message: "Please send requests as application/json")

# Message + errors
render_unsupported_media_type(message: "Please send requests as application/json",errors: { content_type: ["must be application/json, got text/xml"] })

render_range_not_satisfiable — 416

# Message only
render_range_not_satisfiable(message: "Requested byte range is out of bounds")

# Message + errors
render_range_not_satisfiable(message: "Requested byte range is out of bounds", errors: { range: ["exceeds the total file size"] })

render_expectation_failed — 417

# Message only
render_expectation_failed(message: "Expect header value cannot be met")

# Message + errors
render_expectation_failed(message: "Expect header value cannot be met",errors: { expect: ["100-continue is not supported on this endpoint"] })

render_im_a_teapot — 418

# Message only
render_im_a_teapot(message: "I'm a teapot — I cannot brew coffee")

# Message + errors
render_im_a_teapot(message: "I'm a teapot — I cannot brew coffee",errors: { beverage: ["coffee is not supported, try tea"] })

render_misdirected_request — 421

# Message only
render_misdirected_request(message: "Request sent to the wrong server")

# Message + errors
render_misdirected_request(message: "Request sent to the wrong server",errors: { host: ["this server does not handle requests for this host"] })

render_unprocessable — 422 Unprocessable Entity

Validation errors. The most commonly used error helper in Rails APIs.

# Message only
render_unprocessable(message: "Validation failed")

# Message + errors — pass an ActiveModel::Errors object directly
render_unprocessable(message: "Validation failed",errors: user.errors)

# Message + errors — pass a plain hash
render_unprocessable(message: "Invalid data",errors: { name: ["can't be blank"], email: ["is invalid"] })

render_locked — 423

# Message only
render_locked(message: "This record is locked by another user")

# Message + errors
render_locked(message: "This record is locked by another user",errors: { record: ["is currently locked, try again later"] })

render_failed_dependency — 424

# Message only
render_failed_dependency(message: "Prerequisite resource creation failed")

# Message + errors
render_failed_dependency(message: "Prerequisite resource creation failed",errors: { dependency: ["parent record must exist before creating this resource"] })

render_too_early — 425

# Message only
render_too_early(message: "Request may be a replay — rejected for safety")

# Message + errors
render_too_early(message: "Request may be a replay — rejected for safety",errors: { request: ["early data replay detected, resend after handshake"] })

render_upgrade_required — 426

# Message only
render_upgrade_required(message: "Please upgrade to TLS 1.3")

# Message + errors
render_upgrade_required(message: "Please upgrade to TLS 1.3",errors: { protocol: ["TLS 1.2 is no longer supported, upgrade to TLS 1.3"] })

render_precondition_required — 428

# Message only
render_precondition_required(message: "Include an If-Match header with your request")

# Message + errors
render_precondition_required(message: "Include an If-Match header with your request",errors: { if_match: ["header is required to prevent lost updates"] })

render_too_many_requests — 429

# Message only
render_too_many_requests(message: "You have exceeded 100 requests per minute.")

# Message + errors
render_too_many_requests(message: "Rate limit exceeded",errors: { rate_limit: ["100 requests per minute allowed, retry after 60 seconds"] },meta: { retry_after: 60 })

render_request_header_fields_too_large — 431

# Message only
render_request_header_fields_too_large(message: "Cookie header is too large")

# Message + errors
render_request_header_fields_too_large(message: "Cookie header is too large",errors: { cookie: ["must not exceed 4096 bytes"] })
# Message only
render_unavailable_for_legal_reasons(message: "This content is blocked in your region")

# Message + errors
render_unavailable_for_legal_reasons(message: "This content is blocked in your region",errors: { region: ["content is not licensed for distribution in your country"] })

5xx — Server Error Helpers

Two usage patterns for every error helper:

  • Message only — a human-readable string shown to the end user.
  • Message + errors — add errors: when you need to surface internal detail for debugging or logging (e.g. which downstream service failed). errors is a Hash of { field: ["message", ...] }.

render_server_error — 500 Internal Server Error

# Message only
render_server_error(message: "Something went wrong. Our team has been notified.")

# Message + errors
render_server_error(message: "Something went wrong. Our team has been notified.",errors: { server: ["unexpected exception in OrdersController#create"] })

# Common pattern — rescue unexpected exceptions
rescue StandardError => e
  Rails.logger.error(e)
  render_server_error(message: "An unexpected error occurred",errors: { server: [e.message] })

render_not_implemented — 501

# Message only
render_not_implemented(message: "CSV export is coming soon")

# Message + errors
render_not_implemented(message: "CSV export is coming soon",errors: { format: ["csv export is not yet implemented, use json"] })

render_bad_gateway — 502

# Message only
render_bad_gateway(message: "Payment gateway is currently unavailable")

# Message + errors
render_bad_gateway(message: "Payment gateway is currently unavailable",errors: { gateway: ["Stripe returned a 502, please try again"] })

render_service_unavailable — 503

# Message only
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.")

# Message + errors
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.",errors: { service: ["scheduled maintenance window until 03:00 UTC"] },meta: { retry_after: 1800 })

render_gateway_timeout — 504

# Message only
render_gateway_timeout(message: "The payment processor did not respond in time.")

# Message + errors
render_gateway_timeout(message: "The payment processor did not respond in time.",errors: { gateway: ["upstream timeout after 30 seconds, you have not been charged"] })

render_http_version_not_supported — 505

# Message only
render_http_version_not_supported(message: "Only HTTP/1.1 and HTTP/2 are supported")

# Message + errors
render_http_version_not_supported(message: "Only HTTP/1.1 and HTTP/2 are supported",errors: { http_version: ["HTTP/1.0 is not supported"] })

render_variant_also_negotiates — 506

# Message only
render_variant_also_negotiates(message: "Server content-negotiation loop detected")

# Message + errors
render_variant_also_negotiates(message: "Server content-negotiation loop detected",errors: { variant: ["misconfigured content negotiation caused an infinite loop"] })

render_insufficient_storage — 507

# Message only
render_insufficient_storage(message: "Disk quota exceeded on this node")

# Message + errors
render_insufficient_storage(message: "Disk quota exceeded on this node",errors: { storage: ["upload failed, node has 0 bytes remaining"] })

render_loop_detected — 508

# Message only
render_loop_detected(message: "Infinite redirect loop detected")

# Message + errors
render_loop_detected(message: "Infinite redirect loop detected",errors: { redirect: ["request visited the same URL more than 10 times"] })

render_not_extended — 510

# Message only
render_not_extended(message: "Further extensions required to fulfil this request")

# Message + errors
render_not_extended(message: "Further extensions required to fulfil this request",errors: { extension: ["mandatory extension 'auth' is missing from the request"] })

render_network_authentication_required — 511

# Message only
render_network_authentication_required(message: "Sign in to the network portal first")

# Message + errors
render_network_authentication_required(message: "Sign in to the network portal first",errors: { network: ["captive portal authentication required before accessing the API"] })

Real-World Controller Examples

class UsersController < ApplicationController

  def index
    page     = (params[:page]     || 1).to_i
    per_page = (params[:per_page] || 5).to_i

    @users =  Kaminari.paginate_array(User.active).page(page).per(per_page)

    render_ok(
      data:       @users,
      message:    "Users fetched",
      pagination: {
        per_page:         per_page.to_i,
        current_page:     @users.current_page,
        next_page:        @users.next_page,
        prev_page:        @users.prev_page,
        total_pages:      @users.total_pages,
        total_count:      @users.total_count
      }
    )
  end

  def show
    user = User.find(params[:id])
    render_ok(data: user, message: "User found")
  rescue ActiveRecord::RecordNotFound
    render_not_found(message: "User ##{params[:id]} not found",errors: { id: ["no user exists with ID #{params[:id]}"] })
  end

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

  def update
    user = User.find(params[:id])

    unless user == current_user || current_user.admin?
      render_forbidden( message: "You can only update your own profile", errors: { profile: ["you do not have permission to update this profile"] } )
      return
    end

    if user.update(user_params)
      render_ok(data: user, message: "Profile updated")
    else
      render_conflict(message: "Could not update profile", errors: user.errors)
    end
  end

  def destroy
    User.find(params[:id]).destroy!
    render_no_content(message: "Account deleted")
  rescue ActiveRecord::RecordNotFound
    render_gone(message: "This account no longer exists")
  end

end

class PaymentsController < ApplicationController

  def create
    result = PaymentGateway.charge(amount: params[:amount], token: params[:token])
    render_created(data: result, message: "Payment successful")
  rescue PaymentGateway::CardDeclined => e
    render_unprocessable(message: e.message, errors: { card: [e.message] })
  rescue PaymentGateway::Timeout
    render_gateway_timeout( message: "Payment processor timed out. You have not been charged.", errors: { gateway: ["upstream timeout, transaction was not processed"] } )
  rescue PaymentGateway::Error => e
    render_bad_gateway( message: "Payment gateway error: #{e.message}", errors: { gateway: [e.message] })
  end

end

class ReportsController < ApplicationController

  def generate
    ReportJob.perform_later(current_user.id, params[:type])
    render_accepted(
      data:    { estimated_time: "2 minutes" },
      message: "Report is being generated. We will email you when it is ready."
    )
  end

end

Pagination

Respondo does not paginate data for you — your pagination library does that. You build the pagination hash yourself and pass it via pagination:. This keeps the gem simple, dependency-free, and works with any library.

Kaminari

def index
  @users = User.page(params[:page]).per(25)

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

Pagy

def index
  @pagy, @users = pagy(User.all, items: 25)

  render_ok(
    data:       @users,
    message:    "Users fetched",
    pagination: {
      current_page: @pagy.page,
      per_page:     @pagy.items,
      total_pages:  @pagy.pages,
      total_count:  @pagy.count,
      next_page:    @pagy.next,
      prev_page:    @pagy.prev
    }
  )
end

WillPaginate

def index
  @users = User.paginate(page: params[:page], per_page: 25)

  render_ok(
    data:       @users,
    message:    "Users fetched",
    pagination: {
      current_page: @users.current_page,
      per_page:     @users.per_page,
      total_pages:  @users.total_pages,
      total_count:  @users.total_entries,
      next_page:    @users.next_page,
      prev_page:    @users.previous_page
    }
  )
end

No pagination

render_ok(data: @user, message: "User found")
# → meta will have no pagination key at all

Quick Reference Card

# Core
render_success(data:, message:, meta:, pagination:, code:, status:)
render_error(message:, errors:, code:, meta:, status:)

# 1xx — Informational
render_continue(message:, meta:)
render_switching_protocols(message:, meta:)
render_processing(message:, meta:)
render_early_hints(message:, meta:)

# 2xx — Success
render_success(data:, message:, meta:, pagination:, code:, status:)
render_ok(data:, message:, meta:, pagination:)
render_created(data:, message:, meta:, pagination:)
render_accepted(data:, message:, meta:, pagination:)
render_non_authoritative(data:, message:, meta:, pagination:)
render_no_content(message:, meta:, pagination:)
render_reset_content(message:, meta:, pagination:)
render_partial_content(data:, message:, meta:, pagination:)
render_multi_status(data:, message:, meta:, pagination:)
render_already_reported(data:, message:, meta:, pagination:)
render_im_used(data:, message:, meta:, pagination:)

# 3xx — Redirects
render_multiple_choices(data:, message:, meta:, pagination:)
render_moved_permanently(message:, meta:, pagination:)
render_found(message:, meta:, pagination:)
render_see_other(message:, meta:, pagination:)
render_not_modified(message:, meta:, pagination:)
render_temporary_redirect(message:, meta:, pagination:)
render_permanent_redirect(message:, meta:, pagination:)

# 4xx — Client Errors
render_bad_request(message:, errors:, meta:)
render_unauthorized(message:, errors:, meta:)
render_payment_required(message:, errors:, meta:)
render_forbidden(message:, errors:, meta:)
render_not_found(message:, errors:, meta:)
render_method_not_allowed(message:, errors:, meta:)
render_not_acceptable(message:, errors:, meta:)
render_proxy_auth_required(message:, errors:, meta:)
render_request_timeout(message:, errors:, meta:)
render_conflict(message:, errors:, meta:)
render_gone(message:, errors:, meta:)
render_length_required(message:, errors:, meta:)
render_precondition_failed(message:, errors:, meta:)
render_payload_too_large(message:, errors:, meta:)
render_uri_too_long(message:, errors:, meta:)
render_unsupported_media_type(message:, errors:, meta:)
render_range_not_satisfiable(message:, errors:, meta:)
render_expectation_failed(message:, errors:, meta:)
render_im_a_teapot(message:, errors:, meta:)
render_misdirected_request(message:, errors:, meta:)
render_unprocessable(message:, errors:, meta:)
render_locked(message:, errors:, meta:)
render_failed_dependency(message:, errors:, meta:)
render_too_early(message:, errors:, meta:)
render_upgrade_required(message:, errors:, meta:)
render_precondition_required(message:, errors:, meta:)
render_too_many_requests(message:, errors:, meta:)
render_request_header_fields_too_large(message:, errors:, meta:)
render_unavailable_for_legal_reasons(message:, errors:, meta:)

# 5xx — Server Errors
render_server_error(message:, errors:, meta:)
render_not_implemented(message:, errors:, meta:)
render_bad_gateway(message:, errors:, meta:)
render_service_unavailable(message:, errors:, meta:)
render_gateway_timeout(message:, errors:, meta:)
render_http_version_not_supported(message:, errors:, meta:)
render_variant_also_negotiates(message:, errors:, meta:)
render_insufficient_storage(message:, errors:, meta:)
render_loop_detected(message:, errors:, meta:)
render_not_extended(message:, errors:, meta:)
render_network_authentication_required(message:, errors:, meta:)

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

Custom serializer

Respondo.configure do |config|
  # Use ActiveModelSerializers, Blueprinter, Panko, etc.
  config.serializer = ->(obj) { UserSerializer.new(obj).as_json }
end

camelCase for Flutter / JavaScript

Respondo.configure { |c| c.camelize_keys = true }

All keys in the response — including nested meta.pagination — are camelized: current_pagecurrentPage, total_counttotalCount, next_pagenextPage, error_codeerrorCode.

Flutter Integration

// Every response follows the same shape
class ApiResponse<T> {
  final bool success;
  final String message;
  final T? data;
  final Map<String, dynamic> meta;
  final Map<String, dynamic>? errors;

  const ApiResponse({
    required this.success,
    required this.message,
    this.data,
    required this.meta,
    this.errors,
  });
}

Architecture

lib/
├── respondo.rb                    # Entry point, configure, Railtie hook
├── respondo/
│   ├── version.rb                 # VERSION
│   ├── configuration.rb           # Config with defaults
│   ├── serializer.rb              # Auto-detects and serializes any object
│   ├── response_builder.rb        # Assembles the final Hash
│   ├── controller_helpers.rb      # All render_* helpers (1xx–5xx)
│   └── railtie.rb                 # Auto-includes into Rails controllers
└── generators/
    └── respondo/
        └── install/
            └── install_generator.rb   # rails generate respondo:install

Running Tests

bundle install
bundle exec rspec --format documentation

License

MIT