Skip to content
rails-api logo

Rails REST API Specialist

rails-api

REST API specialist for Rails applications. Use when building API endpoints, implementing serialization, API versioning, JWT authentication, or creating API documentation. Focuses on RESTful design, performance, and consistency.

SKILL.md

Full skill instructions

Rails REST API Specialist

Build clean, performant REST APIs with Rails.

When to Use This Skill

  • Creating REST API endpoints
  • Implementing API serialization (ActiveModel::Serializers, Blueprinter)
  • API versioning strategies
  • Token-based authentication (JWT, API keys)
  • API documentation (OpenAPI/​Swagger)
  • Rate limiting and throttling
  • API error handling
  • Performance optimization for APIs

Core Principles

RESTful Design:

  • Use standard HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Resource-based URLs (/​api/​v1/​articles, not /​api/​v1/​get_articles)
  • Proper status codes (200, 201, 404, 422, 500)
  • Consistent response format
  • Stateless requests

🚨 API Versioning is MANDATORY

NEVER create an API without versioning. This is non-negotiable.

Why versioning is required:

  • Protects existing clients from breaking changes
  • Allows independent evolution of API versions
  • Enables deprecation strategies (v1 → v2 → v3)
  • Follows industry best practices
  • Makes your API maintainable long-term

Always start with /​api/​v1/ from day one, even for internal APIs.

Correct vs Wrong Approaches

# ❌ WRONG - No versioning (will cause pain later)
namespace :api do
  resources :articles  # Results in: /​api/​articles
  resources :users     # Impossible to change structure later
end

# ✅ CORRECT - Versioned from start
namespace :api do
  namespace :v1 do
    resources :articles  # Results in: /​api/​v1/​articles
    resources :users     # Can add v2 with breaking changes later
  end
end

Base API Controller

# app/​controllers/​api/​base_controller.rb
class Api::BaseController < ActionController::API
  before_action :authenticate

  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
  rescue_from Pundit::NotAuthorizedError, with: :forbidden

  private

  def not_found(exception)
    render json: { error: exception.message }, status: :not_found
  end

  def unprocessable_entity(exception)
    render json: {
      error: "Validation failed",
      errors: exception.record.errors.full_messages
    }, status: :unprocessable_entity
  end

  def forbidden
    render json: { error: "Access denied" }, status: :forbidden
  end
end

RESTful Controller Pattern

# app/​controllers/​api/​v1/​articles_controller.rb
class Api::V1::ArticlesController < Api::BaseController
  before_action :set_article, only: [:show, :update, :destroy]

  # GET /​api/​v1/​articles
  def index
    @articles = Article.published
      .page(params[:page])
      .per(params[:per_page] || 20)

    render json: @articles,
      meta: pagination_meta(@articles),
      each_serializer: ArticleSerializer
  end

  # GET /​api/​v1/​articles/:id
  def show
    render json: @article, serializer: ArticleDetailSerializer
  end

  # POST /​api/​v1/​articles
  def create
    @article = current_user.articles.build(article_params)

    if @article.save
      render json: @article,
        status: :created,
        location: api_v1_article_url(@article)
    else
      render json: { errors: @article.errors },
        status: :unprocessable_entity
    end
  end

  # PATCH/​PUT /​api/​v1/​articles/:id
  def update
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors },
        status: :unprocessable_entity
    end
  end

  # DELETE /​api/​v1/​articles/:id
  def destroy
    @article.destroy
    head :no_content
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :body, :published)
  end

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

Serialization

# app/​serializers/​article_serializer.rb
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :excerpt, :published_at, :created_at

  belongs_to :author, serializer: UserSerializer
  has_many :tags

  def excerpt
    object.body.truncate(200)
  end
end

Response Format

Success:

{
  "data": {
    "id": 123,
    "type": "articles",
    "attributes": {
      "title": "Article Title",
      "excerpt": "Article excerpt..."
    },
    "relationships": {
      "author": { "data": { "id": 1, "type": "users" } }
    }
  },
  "meta": {
    "current_page": 1,
    "total_pages": 10
  }
}

Error:

{
  "error": "Validation failed",
  "errors": [
    "Title can't be blank",
    "Body is too short"
  ]
}

Authentication

JWT Authentication

# Gemfile
gem 'jwt'

# app/​controllers/​api/​v1/​auth_controller.rb
class Api::V1::AuthController < Api::BaseController
  skip_before_action :authenticate, only: [:login]

  def login
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      token = encode_token(user_id: user.id)
      render json: {
        token: token,
        user: UserSerializer.new(user),
        expires_at: 24.hours.from_now
      }
    else
      render json: { error: 'Invalid credentials' },
        status: :unauthorized
    end
  end

  private

  def encode_token(payload)
    payload[:exp] = 24.hours.from_now.to_i
    JWT.encode(payload, Rails.application.credentials.secret_key_base)
  end
end

# Update authenticate method in base controller
def authenticate
  token = request.headers['Authorization']&.split(' ')&.last
  return render json: { error: 'No token' }, status: :unauthorized unless token

  begin
    decoded = JWT.decode(token, Rails.application.credentials.secret_key_base)[0]
    @current_user = User.find(decoded['user_id'])
  rescue JWT::ExpiredSignature
    render json: { error: 'Token expired' }, status: :unauthorized
  rescue JWT::DecodeError
    render json: { error: 'Invalid token' }, status: :unauthorized
  end
end

API Key Authentication

# app/​models/​user.rb
class User < ApplicationRecord
  before_create :generate_api_token

  private

  def generate_api_token
    self.api_token = SecureRandom.hex(32)
  end
end

# app/​controllers/​api/​base_controller.rb
def authenticate
  authenticate_or_request_with_http_token do |token, options|
    @current_user = User.find_by(api_token: token)
  end
end

Rate Limiting

# Gemfile
gem 'rack-attack'

# config/​initializers/​rack_attack.rb
class Rack::Attack
  # Throttle all API requests by IP
  throttle('req/​ip', limit: 300, period: 5.minutes) do |req|
    req.ip if req.path.start_with?('/​api/')
  end

  # Throttle login attempts
  throttle('logins/​email', limit: 5, period: 20.seconds) do |req|
    if req.path == '/​api/​v1/​auth/​login' && req.post?
      req.params['email'].presence
    end
  end
end

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

Filtering and Pagination

def index
  @articles = Article.all
  @articles = apply_filters(@articles)
  @articles = @articles.page(params[:page]).per(params[:per_page] || 20)

  render json: @articles, meta: pagination_meta(@articles)
end

private

def apply_filters(scope)
  scope = scope.where(status: params[:status]) if params[:status].present?
  scope = scope.where(author_id: params[:author_id]) if params[:author_id].present?
  scope = scope.where('created_at >= ?', params[:from_date]) if params[:from_date].present?
  scope
end

Performance

Avoid N+1 Queries

def index
  @articles = Article.includes(:author, :tags)
    .page(params[:page])
    .per(params[:per_page])

  render json: @articles
end

HTTP Caching

def show
  @article = Article.find(params[:id])

  if stale?(last_modified: @article.updated_at, etag: @article)
    render json: @article
  end
end

Testing APIs

# spec/​requests/​api/​v1/​articles_spec.rb
RSpec.describe 'Articles API', type: :request do
  let(:user) { create(:user) }
  let(:token) { JWT.encode({ user_id: user.id }, Rails.application.credentials.secret_key_base) }
  let(:headers) { { 'Authorization' => "Bearer #{token}" } }

  describe 'GET /​api/​v1/​articles' do
    it 'returns articles' do
      create_list(:article, 3, :published)

      get '/​api/​v1/​articles', headers: headers

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

  describe 'POST /​api/​v1/​articles' do
    let(:valid_params) do
      { article: { title: 'New Article', body: 'Content' } }
    end

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

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

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

      expect(response).to have_http_status(:unprocessable_entity)
      expect(JSON.parse(response.body)['errors']).to be_present
    end
  end
end

Best Practices

✅ Do

  • ALWAYS version your API (v1, v2, v3) - MANDATORY, not optional
  • Use proper HTTP status codes (200, 201, 404, 422, 500)
  • Implement authentication and authorization
  • Add rate limiting to prevent abuse
  • Document your API (OpenAPI/​Swagger)
  • Use serializers for consistent responses
  • Implement pagination for collections
  • Handle errors gracefully with consistent format
  • Use ETags for caching
  • Test all endpoints with request specs

❌ Don't

  • NEVER create API without versioning - This will cause pain later
  • Create routes like /​api/​articles instead of /​api/​v1/​articles
  • Expose internal IDs without consideration
  • Return sensitive data (passwords, tokens, internal fields)
  • Use GET for state-changing operations
  • Return inconsistent response formats
  • Skip authentication on "internal" endpoints
  • Expose database errors to clients
  • Make breaking changes to existing API versions

Reference Documentation

For comprehensive examples and advanced patterns:

  • Full API guide: api-reference.md (detailed auth, documentation, testing, all patterns)

Remember: A good API is versioned from day one, consistent, well-documented, secure, and performant. API versioning is not optional—it's a fundamental requirement.

More Security skills

supabase logo
Security

supabase

Handles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...

2.7K 308.9K
View

Comprehensive guides and best practices for Neon Serverless Postgres, covering setup, connection methods, authentication, and platform APIs.

98 216.2K
View

Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules.

462 163.8K
View

A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.

462 127.7K
View

Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.

462 125.1K
View

Complete Better Auth server and client setup with database adapters, session management, plugins, and security configuration.

222 118.2K
View

Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".

31.9K 116.1K
View
cloudflare logo
Security

cloudflare

Complete Cloudflare platform integration with decision trees for compute, storage, AI, networking, security, and infrastructure-as-code.

3K 110.7K
View

Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.

253 103K
View
gws-gmail logo
Security

gws-gmail

Send, read, and manage Gmail messages, drafts, labels, and account settings.

31.2K 78.9K
View

Comprehensive website auditing across 230+ rules in 21 categories including SEO, performance, security, and accessibility.

94 72.3K
View
gws-shared logo
Security

gws-shared

Shared authentication, CLI syntax, and output formatting patterns for gws Google Workspace commands.

31.2K 63.4K
View

Security AI tools

StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
GeoSpy logo
Security

GeoSpy

GeoSpy: Pricing, Features, FAQs, and Alternatives for AI Teams

Free
View

Detect AI-generated voices to protect against audio fraud.

Paid
View

Ensure Your Content's Originality with AI Plagiarism Checker

Freemium
View
Img Upscaler logo
Security

Img Upscaler

Upscale images by 400% without quality loss

Freemium
View
AICheatCheck logo
Security

AICheatCheck

Accurately Detect AI-Generated Content with TheChecker.AI

Free
View
D
Security

Detect GPT

Chrome extension that detects and flags AI-generated content

Free
View