thibautbaissac/rails_ai_agents/api-patterns

Claude Code and agent skill by thibautbaissac/rails_ai_agents

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Builds REST APIs using respond_to blocks with Jbuilder templates following the 37signals same-controllers-different-formats philosophy. Use when adding API endpoints, JSON responses, token authentication, pagination, or when user mentions API, JSON, REST, or Jbuilder. WHEN NOT: For HTML-only controllers (use crud-patterns), for webhook delivery (use event-tracking).

Install

npx vskill@latest install thibautbaissac/rails_ai_agents/api-patterns

SKILL.md

API Patterns

Philosophy: Same Controllers, Different Formats

  • One controller serves both HTML (web) and JSON (API)
  • Use respond_to blocks for format-specific responses
  • 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:

bash
# 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

ruby
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.

ruby
# 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.

ruby
# 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

ruby
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

ruby
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.

ruby
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:

ruby
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

ruby
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 (respond_to blocks)
  • 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 respond_to works
  • 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)

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
589
Repository forks
67
7-day trend score
32

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/thibautbaissac/rails_ai_agents/api-patterns/badge)](https://verified-skill.com/skills/thibautbaissac/rails_ai_agents/api-patterns)

Something wrong with this skill or its page? Tell us.