Install
$ agentstack add skill-shoebtamboli-rails-claude-skills-rails-api-controllers ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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
ApplicationControllerparent withActionController::APIfor 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:
# 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:
# 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:
# 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):
# 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
# 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:
# 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:
# 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):
# 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:
# 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
# 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.
# 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:
Gems & Libraries:
- jwt - JSON Web Token implementation
- rack-cors - CORS middleware
- rack-attack - Rate limiting and throttling
- kaminari - Pagination
- pagy - Fast pagination
- pundit - Authorization
API Documentation:
- rswag - OpenAPI/Swagger docs for Rails APIs
- 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
- Source: Shoebtamboli/railsclaude_skills
- License: MIT
- Homepage: https://rubygems.org/gems/railsclaudeskills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.