Rails_ai_agents api-patterns
install
source · Clone the upstream repo
git clone https://github.com/ThibautBaissac/rails_ai_agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ThibautBaissac/rails_ai_agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude_37signals/skills/api-patterns" ~/.claude/skills/thibautbaissac-rails-ai-agents-api-patterns && rm -rf "$T"
manifest:
.claude_37signals/skills/api-patterns/SKILL.mdsource content
API Patterns
Philosophy: Same Controllers, Different Formats
- One controller serves both HTML (web) and JSON (API)
- Use
blocks for format-specific responsesrespond_to - RESTful routes only (no GraphQL, no custom endpoints unless necessary)
- Jbuilder for JSON templates (like ERB for HTML)
- HTTP status codes for success/errors
- Token-based authentication for API (not OAuth unless required)
Project Knowledge
Stack: Jbuilder for JSON views, RESTful routes, token-based API auth, same controllers for HTML and JSON, HTTP caching with ETags for API.
Multi-tenancy: API uses same account scoping (
/accounts/:account_id/...),
token scoped to account.
Commands:
# Generate API token model rails generate model ApiToken user:references account:references \ token:string last_used_at:datetime # Test API endpoints curl -H "Authorization: Bearer TOKEN" \ -H "Accept: application/json" \ http://localhost:3000/boards
Pattern 1: Respond To Blocks
class BoardsController < ApplicationController before_action :set_board, only: [:show, :edit, :update, :destroy] def index @boards = Current.account.boards.includes(:creator).order(created_at: :desc) respond_to do |format| format.html # renders index.html.erb format.json # renders index.json.jbuilder end end def show respond_to do |format| format.html format.json end end def create @board = Current.account.boards.build(board_params) @board.creator = Current.user respond_to do |format| if @board.save format.html { redirect_to @board, notice: "Board created" } format.json { render :show, status: :created, location: @board } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @board.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @board.update(board_params) format.html { redirect_to @board, notice: "Board updated" } format.json { render :show, status: :ok } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @board.errors, status: :unprocessable_entity } end end end def destroy @board.destroy respond_to do |format| format.html { redirect_to boards_path, notice: "Board deleted" } format.json { head :no_content } end end private def set_board @board = Current.account.boards.find(params[:id]) end def board_params params.require(:board).permit(:name, :description) end end
Pattern 2: Jbuilder Templates
See @references/jbuilder-templates.md for full details.
# app/views/boards/index.json.jbuilder json.array! @boards do |board| json.extract! board, :id, :name, :description, :created_at, :updated_at json.creator do json.extract! board.creator, :id, :name, :email end json.url board_url(board, format: :json) end # app/views/boards/show.json.jbuilder json.extract! @board, :id, :name, :description, :created_at, :updated_at json.creator do json.extract! @board.creator, :id, :name, :email end json.cards @board.cards, partial: "cards/card", as: :card json.url board_url(@board, format: :json) # app/views/cards/_card.json.jbuilder json.extract! card, :id, :title, :description, :created_at, :updated_at json.creator do json.extract! card.creator, :id, :name end json.url board_card_url(card.board, card, format: :json)
Pattern 3: API Token Authentication
See @references/api-auth.md for full details.
# app/models/api_token.rb class ApiToken < ApplicationRecord belongs_to :user belongs_to :account has_secure_token :token, length: 32 validates :name, presence: true validates :token, presence: true, uniqueness: true scope :active, -> { where(active: true) } def use! touch(:last_used_at) end end # app/controllers/concerns/api_authenticatable.rb module ApiAuthenticatable extend ActiveSupport::Concern included do before_action :authenticate_from_token, if: :api_request? end private def api_request? request.format.json? end def authenticate_from_token token = request.headers["Authorization"] &.match(/Bearer (.+)/)&.captures&.first if token @api_token = ApiToken.active.find_by(token: token) if @api_token @api_token.use! Current.user = @api_token.user Current.account = @api_token.account else render json: { error: "Unauthorized" }, status: :unauthorized end else render json: { error: "Unauthorized" }, status: :unauthorized end end end # app/controllers/application_controller.rb class ApplicationController < ActionController::Base include ApiAuthenticatable skip_before_action :verify_authenticity_token, if: :api_request? before_action :authenticate_user!, unless: :api_request? end
Pattern 4: Error Handling
module ApiErrorHandling extend ActiveSupport::Concern included do rescue_from ActiveRecord::RecordNotFound, with: :render_not_found rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity rescue_from ActionController::ParameterMissing, with: :render_bad_request end private def render_not_found(exception) respond_to do |format| format.html { raise exception } format.json { render json: { error: "Not found" }, status: :not_found } end end def render_unprocessable_entity(exception) respond_to do |format| format.html { raise exception } format.json do render json: { error: "Validation failed", details: exception.record.errors.as_json }, status: :unprocessable_entity end end end def render_bad_request(exception) respond_to do |format| format.html { raise exception } format.json do render json: { error: "Bad request", message: exception.message }, status: :bad_request end end end end
Pattern 5: HTTP Caching for API
def index @boards = Current.account.boards.includes(:creator).order(created_at: :desc) respond_to do |format| format.html format.json do if stale?(@boards) render :index end end end end def show @board = Current.account.boards.find(params[:id]) respond_to do |format| format.html format.json do if stale?(@board) render :show end end end end
Pattern 6: Pagination
See @references/api-versioning.md for versioning details.
def index @boards = Current.account.boards.includes(:creator) .order(created_at: :desc) .page(params[:page]) .per(params[:per_page] || 25) respond_to do |format| format.html format.json do response.headers["X-Total-Count"] = @boards.total_count.to_s response.headers["X-Page"] = @boards.current_page.to_s response.headers["X-Per-Page"] = @boards.limit_value.to_s render :index end end end # app/views/boards/index.json.jbuilder json.boards @boards do |board| json.extract! board, :id, :name, :description, :created_at json.url board_url(board, format: :json) end json.pagination do json.current_page @boards.current_page json.per_page @boards.limit_value json.total_pages @boards.total_pages json.total_count @boards.total_count json.next_page boards_url(page: @boards.next_page, format: :json) if @boards.next_page json.prev_page boards_url(page: @boards.prev_page, format: :json) if @boards.prev_page end
Cursor-based alternative:
def index @boards = Current.account.boards.order(created_at: :desc) @boards = @boards.where("created_at < ?", Time.zone.parse(params[:before])) if params[:before] @boards = @boards.limit(params[:limit] || 25) respond_to do |format| format.html format.json end end
Pattern 7: Batch Operations
class Cards::BatchController < ApplicationController before_action :set_board def update results, errors = [], [] batch_params[:cards].each do |card_params| card = @board.cards.find(card_params[:id]) if card.update(card_params.except(:id)) results << card else errors << { id: card.id, errors: card.errors } end end respond_to do |format| format.json do if errors.empty? render json: { success: true, cards: results }, status: :ok else render json: { success: false, errors: errors }, status: :unprocessable_entity end end end end end
Boundaries
Always
- Use same controllers for HTML and JSON (
blocks)respond_to - Use Jbuilder for JSON views (not inline JSON in controllers)
- Return proper HTTP status codes (201, 404, 422, etc.)
- Implement token-based authentication for API
- Include resource URLs in JSON responses
- Scope all API requests to
Current.account - Use ETags for HTTP caching
- Test both HTML and JSON responses
Ask First
- Whether to version API (most apps don't need it initially)
- Pagination strategy (page-based vs cursor-based)
- Whether to support batch operations
- Rate limiting requirements
Never
- Use GraphQL (stick to REST)
- Create separate API controllers when
worksrespond_to - Use Active Model Serializers (use Jbuilder)
- Inline JSON in controllers (use views)
- Skip authentication for API endpoints
- Return HTML errors for JSON requests
- Use session-based auth for API (use tokens)