# Rails Api Controllers

> RESTful API controller patterns for Ruby on Rails. Use when: (1) Building JSON APIs, (2) API versioning, (3) Error handling and status codes, (4) Authentication with tokens/JWT, (5) Rate limiting, (6) CORS configuration, (7) Pagination and filtering, (8) API documentation, (9) Testing API endpoints

- **Type:** Skill
- **Install:** `agentstack add skill-shoebtamboli-rails-claude-skills-rails-api-controllers`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Shoebtamboli](https://agentstack.voostack.com/s/shoebtamboli)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Shoebtamboli](https://github.com/Shoebtamboli)
- **Source:** https://github.com/Shoebtamboli/rails_claude_skills/tree/main/lib/generators/claude/skills_library/rails-api-controllers
- **Website:** https://rubygems.org/gems/rails_claude_skills

## Install

```sh
agentstack add skill-shoebtamboli-rails-claude-skills-rails-api-controllers
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Rails API Controllers

Build production-ready RESTful JSON APIs with Rails. This skill covers API controller patterns, versioning, authentication, error handling, and best practices for modern API development.

- Building JSON APIs for mobile apps, SPAs, or third-party integrations
- Creating microservices or API-first applications
- Versioning APIs for backward compatibility
- Implementing token-based authentication (JWT, API keys)
- Adding rate limiting and throttling
- Configuring CORS for cross-origin requests
- Implementing pagination, filtering, and sorting
- Testing API endpoints with RSpec

- **RESTful Design** - Follow REST conventions for predictable, maintainable APIs
- **Proper Status Codes** - Use correct HTTP status codes for all responses
- **Error Handling** - Consistent error responses with meaningful messages
- **Versioning** - Support multiple API versions simultaneously
- **Authentication** - Token-based auth without sessions or cookies
- **Performance** - Efficient JSON rendering and database queries
- **Documentation** - Auto-generated API docs with tools like Rswag

Before completing API controller work:
- ✅ Proper HTTP status codes used (200, 201, 204, 400, 401, 403, 404, 422, 500)
- ✅ Consistent JSON response structure
- ✅ Authentication/authorization implemented
- ✅ Error handling covers all edge cases
- ✅ API tests passing (request specs)
- ✅ CORS configured if needed
- ✅ Rate limiting configured for production
- ✅ API documentation generated/updated

- Use `ApplicationController` parent with `ActionController::API` for API-only apps
- Return proper HTTP status codes for all responses
- Use consistent JSON structure across all endpoints
- Implement authentication via tokens (JWT, API keys), NOT sessions
- Version APIs via URL path (`/api/v1/`) or Accept header
- Handle errors consistently with JSON error responses
- Use strong parameters for input validation
- Test with request specs, not controller specs
- Document APIs with OpenAPI/Swagger
- Implement rate limiting to prevent abuse

---

## API-Only Rails Setup

Create new API-only Rails application

**Generate API-Only App:**

```bash
# New API-only Rails app (skips views, helpers, assets)
rails new my_api --api

# Or add to existing app
# config/application.rb
module MyApi
  class Application 

---

## RESTful API Design

Standard RESTful API controller with all CRUD actions

```ruby
# app/controllers/api/v1/articles_controller.rb
module Api
  module V1
    class ArticlesController 

Use correct HTTP status codes for API responses

**Common Status Codes:**

| Code | Symbol | Usage |
|------|--------|-------|
| 200 | `:ok` | Successful GET, PATCH, PUT |
| 201 | `:created` | Successful POST (resource created) |
| 204 | `:no_content` | Successful DELETE (no response body) |
| 400 | `:bad_request` | Invalid request syntax, missing parameters |
| 401 | `:unauthorized` | Missing or invalid authentication |
| 403 | `:forbidden` | Authenticated but lacks permission |
| 404 | `:not_found` | Resource doesn't exist |
| 422 | `:unprocessable_entity` | Validation errors |
| 429 | `:too_many_requests` | Rate limit exceeded |
| 500 | `:internal_server_error` | Server error |

**Examples:**

```ruby
# Success responses
render json: @article, status: :ok                    # 200
render json: @article, status: :created               # 201
head :no_content                                       # 204

# Error responses
render json: { error: 'Bad request' }, status: :bad_request              # 400
render json: { error: 'Unauthorized' }, status: :unauthorized            # 401
render json: { error: 'Forbidden' }, status: :forbidden                  # 403
render json: { error: 'Not found' }, status: :not_found                  # 404
render json: { error: 'Validation failed' }, status: :unprocessable_entity  # 422
```

**Why:** Correct status codes help API clients handle responses appropriately and provide clear semantics about what happened.

---

## API Versioning

Version APIs via URL namespace for backward compatibility

**Directory Structure:**

```
app/controllers/
└── api/
    ├── v1/
    │   ├── articles_controller.rb
    │   └── users_controller.rb
    └── v2/
        ├── articles_controller.rb
        └── users_controller.rb
```

**V1 Controller:**

```ruby
# app/controllers/api/v1/articles_controller.rb
module Api
  module V1
    class ArticlesController 

Breaking API changes without versioning

```ruby
# ❌ WRONG - Breaking existing clients
class Api::ArticlesController 

```ruby
# ✅ CORRECT - New version for breaking changes
module Api
  module V1
    class ArticlesController 

**Why bad:** Breaking changes without versioning break existing API clients. Always version when changing response structure or behavior.

---

## Authentication & Authorization

Token-based authentication for stateless APIs

**User Model:**

```ruby
# app/models/user.rb
class User 

JWT (JSON Web Token) authentication for APIs

**Setup:**

```ruby
# Gemfile
gem 'jwt'

# lib/json_web_token.rb
class JsonWebToken
  SECRET_KEY = Rails.application.credentials.secret_key_base

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    body = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new(body)
  rescue JWT::DecodeError, JWT::ExpiredSignature
    nil
  end
end
```

**Application Controller:**

```ruby
# app/controllers/application_controller.rb
class ApplicationController 

---

## Pagination, Filtering & Sorting

Paginate API responses with Kaminari or Pagy

**With Kaminari:**

```ruby
# Gemfile
gem 'kaminari'

# app/controllers/api/v1/articles_controller.rb
def index
  page = params[:page] || 1
  per_page = params[:per_page] || 20

  @articles = Article.page(page).per(per_page)

  render json: {
    data: @articles,
    meta: {
      current_page: @articles.current_page,
      next_page: @articles.next_page,
      prev_page: @articles.prev_page,
      total_pages: @articles.total_pages,
      total_count: @articles.total_count
    }
  }
end
```

**With Pagy (Faster):**

```ruby
# Gemfile
gem 'pagy'

# app/controllers/application_controller.rb
include Pagy::Backend

# app/controllers/api/v1/articles_controller.rb
def index
  pagy, articles = pagy(Article.all, items: params[:per_page] || 20)

  render json: {
    data: articles,
    meta: {
      current_page: pagy.page,
      total_pages: pagy.pages,
      total_count: pagy.count,
      per_page: pagy.items
    }
  }
end
```

**Why:** Pagination prevents loading large datasets into memory. Include metadata so clients know how to fetch more pages.

Allow clients to filter and sort resources

```ruby
# app/controllers/api/v1/articles_controller.rb
def index
  @articles = Article.all

  # Filtering
  @articles = @articles.where(status: params[:status]) if params[:status].present?
  @articles = @articles.where(category: params[:category]) if params[:category].present?
  @articles = @articles.where('created_at >= ?', params[:from_date]) if params[:from_date].present?

  # Searching
  @articles = @articles.where('title ILIKE ?', "%#{params[:q]}%") if params[:q].present?

  # Sorting
  sort_column = params[:sort_by] || 'created_at'
  sort_direction = params[:order] || 'desc'
  @articles = @articles.order("#{sort_column} #{sort_direction}")

  # Pagination
  @articles = @articles.page(params[:page]).per(params[:per_page] || 20)

  render json: {
    data: @articles,
    meta: pagination_meta(@articles)
  }
end

private

def pagination_meta(collection)
  {
    current_page: collection.current_page,
    total_pages: collection.total_pages,
    total_count: collection.total_count
  }
end
```

**Example Requests:**

```bash
# Filter by status
GET /api/v1/articles?status=published

# Search by title
GET /api/v1/articles?q=rails

# Sort by created_at descending
GET /api/v1/articles?sort_by=created_at&order=desc

# Combine filters, search, sort, and pagination
GET /api/v1/articles?status=published&q=rails&sort_by=title&order=asc&page=2&per_page=50
```

**Why:** Flexible filtering and sorting let clients fetch exactly what they need without loading unnecessary data.

---

## CORS Configuration

Configure CORS to allow cross-origin API requests

**Setup:**

```ruby
# Gemfile
gem 'rack-cors'

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'example.com', 'localhost:3000'  # Whitelist specific origins

    resource '/api/*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true,
      max_age: 86400  # Cache preflight for 24 hours
  end
end
```

**Development (Allow All Origins):**

```ruby
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    if Rails.env.development?
      origins '*'  # Allow all in development
    else
      origins ENV['ALLOWED_ORIGINS']&.split(',') || 'example.com'
    end

    resource '/api/*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

**Why:** CORS is required when frontend (SPA, mobile app) and API are on different domains. Whitelist specific origins in production for security.

---

## Rate Limiting

Implement rate limiting to prevent API abuse

**With Rack::Attack:**

```ruby
# Gemfile
gem 'rack-attack'

# config/initializers/rack_attack.rb
class Rack::Attack
  # Throttle all requests by IP (60 requests per minute)
  throttle('req/ip', limit: 60, period: 1.minute) do |req|
    req.ip if req.path.start_with?('/api/')
  end

  # Throttle POST requests by IP (10 per minute)
  throttle('req/ip/post', limit: 10, period: 1.minute) do |req|
    req.ip if req.path.start_with?('/api/') && req.post?
  end

  # Throttle authenticated requests by user token
  throttle('req/token', limit: 100, period: 1.minute) do |req|
    if req.path.start_with?('/api/')
      token = req.env['HTTP_AUTHORIZATION']&.split(' ')&.last
      User.find_by(api_token: token)&.id if token
    end
  end

  # Custom response for throttled requests
  self.throttled_responder = lambda do |env|
    [
      429,
      { 'Content-Type' => 'application/json' },
      [{ error: 'Rate limit exceeded. Try again later.' }.to_json]
    ]
  end
end

# config/application.rb
config.middleware.use Rack::Attack
```

**Why:** Rate limiting prevents abuse, protects server resources, and ensures fair usage across all API clients.

---

## Error Handling

Standardized error response format

```ruby
# app/controllers/application_controller.rb
class ApplicationController 

---

## Testing API Endpoints

Test API endpoints with RSpec request specs

```ruby
# spec/requests/api/v1/articles_spec.rb
require 'rails_helper'

RSpec.describe 'Api::V1::Articles', type: :request do
  let(:user) { create(:user) }
  let(:headers) { { 'Authorization' => "Token #{user.api_token}" } }

  describe 'GET /api/v1/articles' do
    let!(:articles) { create_list(:article, 3, :published) }

    it 'returns all published articles' do
      get '/api/v1/articles', headers: headers

      expect(response).to have_http_status(:ok)
      expect(json_response['data'].size).to eq(3)
    end

    it 'filters by status' do
      draft = create(:article, status: :draft)

      get '/api/v1/articles', params: { status: 'draft' }, headers: headers

      expect(response).to have_http_status(:ok)
      expect(json_response['data'].size).to eq(1)
      expect(json_response['data'].first['id']).to eq(draft.id)
    end

    it 'paginates results' do
      create_list(:article, 25)

      get '/api/v1/articles', params: { page: 2, per_page: 10 }, headers: headers

      expect(response).to have_http_status(:ok)
      expect(json_response['data'].size).to eq(10)
      expect(json_response['meta']['current_page']).to eq(2)
    end
  end

  describe 'POST /api/v1/articles' do
    let(:valid_attributes) { { article: { title: 'Test', body: 'Content' } } }

    it 'creates a new article' do
      expect {
        post '/api/v1/articles', params: valid_attributes, headers: headers
      }.to change(Article, :count).by(1)

      expect(response).to have_http_status(:created)
      expect(json_response['title']).to eq('Test')
      expect(response.location).to be_present
    end

    it 'returns errors for invalid data' do
      post '/api/v1/articles', params: { article: { title: '' } }, headers: headers

      expect(response).to have_http_status(:unprocessable_entity)
      expect(json_response['error']).to eq('Failed to create article')
      expect(json_response['details']).to include("Title can't be blank")
    end
  end

  describe 'DELETE /api/v1/articles/:id' do
    let!(:article) { create(:article) }

    it 'deletes the article' do
      expect {
        delete "/api/v1/articles/#{article.id}", headers: headers
      }.to change(Article, :count).by(-1)

      expect(response).to have_http_status(:no_content)
      expect(response.body).to be_empty
    end
  end

  describe 'authentication' do
    it 'returns 401 without token' do
      get '/api/v1/articles'

      expect(response).to have_http_status(:unauthorized)
      expect(json_response['error']).to eq('Unauthorized')
    end

    it 'returns 401 with invalid token' do
      get '/api/v1/articles', headers: { 'Authorization' => 'Token invalid' }

      expect(response).to have_http_status(:unauthorized)
    end
  end

  private

  def json_response
    JSON.parse(response.body)
  end
end
```

**Why:** Request specs test the full HTTP request/response cycle including routing, authentication, and JSON parsing. More realistic than controller specs.

---

```ruby
# spec/support/request_helpers.rb
module RequestHelpers
  def json_response
    JSON.parse(response.body)
  end

  def auth_headers(user)
    { 'Authorization' => "Token #{user.api_token}" }
  end
end

RSpec.configure do |config|
  config.include RequestHelpers, type: :request
end

# spec/requests/api/v1/authentication_spec.rb
RSpec.describe 'Api::V1::Authentication', type: :request do
  describe 'POST /api/v1/auth' do
    let(:user) { create(:user, email: 'test@example.com', password: 'password') }

    it 'returns token with valid credentials' do
      post '/api/v1/auth', params: { email: 'test@example.com', password: 'password' }

      expect(response).to have_http_status(:ok)
      expect(json_response['token']).to be_present
      expect(json_response['user']['email']).to eq('test@example.com')
    end

    it 'returns error with invalid credentials' do
      post '/api/v1/auth', params: { email: 'test@example.com', password: 'wrong' }

      expect(response).to have_http_status(:unauthorized)
      expect(json_response['error']).to eq('Invalid email or password')
    end
  end
end
```

---

- rails-ai:models - Model patterns for API resources
- rails-ai:serializers - JSON serialization (ActiveModelSerializers, Blueprinter)
- rails-ai:testing - Testing patterns for API endpoints
- rails-ai:auth-with-devise - Token-based authentication with Devise
- rails-ai:jobs - Background processing for async API operations

**Official Documentation:**
- [Rails Guides - API-Only Applications](https://guides.rubyonrails.org/api_app.html)
- [Rails API Documentation](https://api.rubyonrails.org/)

**Gems & Libraries:**
- [jwt](https://github.com/jwt/ruby-jwt) - JSON Web Token implementation
- [rack-cors](https://github.com/cyu/rack-cors) - CORS middleware
- [rack-attack](https://github.com/rack/rack-attack) - Rate limiting and throttling
- [kaminari](https://github.com/kaminari/kaminari) - Pagination
- [pagy](https://github.com/ddnexus/pagy) - Fast pagination
- [pundit](https://github.com/varvet/pundit) - Authorization

**API Documentation:**
- [rswag](https://github.com/rswag/rswag) - OpenAPI/Swagger docs for Rails APIs
- [apipie-rails](https://github.com/Apipie/apipie-rails) - API documentation tool

**Best Practices:**
- [R

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Shoebtamboli](https://github.com/Shoebtamboli)
- **Source:** [Shoebtamboli/rails_claude_skills](https://github.com/Shoebtamboli/rails_claude_skills)
- **License:** MIT
- **Homepage:** https://rubygems.org/gems/rails_claude_skills

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-shoebtamboli-rails-claude-skills-rails-api-controllers
- Seller: https://agentstack.voostack.com/s/shoebtamboli
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
