agentleFS
dilolabsdilolabs/nosia/.claude/skills/api-patterns/SKILL.md

api-patterns

Skill211 starsChanged 5 months agoMITView on GitHub

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

14 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use intelligent-routing, nextjs-react-expert and 5 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • SKB3002Skill

    intelligent-routing

    Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.

    Also in 5 projects that use this

  • SKB3002Skill

    nextjs-react-expert

    React and Next.js performance optimization from Vercel Engineering. Use when building React components, optimizing performance, eliminating waterfalls, reducing bundle size, reviewing code for performance issues, or implementing server/client-side optimizations.

    Also in 3 projects that use this

  • xfstudioSkill

    rust-pro

    Master Rust 1.75+ with modern async patterns, advanced type system

    Also in 5 projects that use this

  • jornalistainclusivoSkill

    design-spec

    How to author a DESIGN.md file — the machine-readable design-token + human-rationale format that must exist before any UI is built. YAML front-matter token schema (colors, typography, spacing, rounded, components), type system, token references, and canonical section order.

    Also in 3 projects that use this

  • zzfancitizenSkill

    verify-changes

    Verify code changes do not break the project by running tests, linter checks, and import validation. Use after making any code changes, editing Python files, modifying imports, updating dependencies, or refactoring.

    Also in 3 projects that use this

  • VercelSkill

    web-design-guidelines

    Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".

    Also in 5 projects that use this

The file

We split this skill into the rules it gives an agent

9 rules. 0 are common, 6 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 14 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use respond_to blocks for format-specific responses

    line 19
  2. Very specific:

    Use same controllers for HTML and JSON (respond_to blocks)

    line 371
  3. Distinctive:

    Use Jbuilder for JSON views (not inline JSON in controllers)

    line 372
  4. Distinctive:

    Use ETags for HTTP caching

    line 377
  5. Distinctive:

    Test both HTML and JSON responses

    line 378
  6. Distinctive:

    Use GraphQL (stick to REST)

    line 387
  7. Very specific:

    Create separate API controllers when respond_to works

    line 388
  8. Distinctive:

    Use Active Model Serializers (use Jbuilder)

    line 389
  9. Distinctive:

    Use session-based auth for API (use tokens)

    line 393

“Common” is measured: kept by at least 6 in 10 of the 14 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
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:

# 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 (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)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.