Introduction
Ruby on Rails remains a leading framework in 2026 for rapidly building high-performance REST APIs. This intermediate tutorial guides you through creating a complete task management API. You will learn how to structure Active Record models, expose secure JSON endpoints, and implement robust tests. The focus is on modern best practices: JSON:API serialization, server-side validation, and standardized error handling. This guide targets developers with existing Rails experience who want to reach a professional level.
Prerequisites
- Ruby 3.3+ and Rails 8.0+
- Basic knowledge of Active Record and MVC
- PostgreSQL installed
- Testing tool such as RSpec or Minitest
Project Initialization
rails new task_api --api -d postgresql
cd task_api
rails db:createThe --api flag generates a lightweight structure without views. PostgreSQL is used for its reliability with Rails migrations. Run this command in a clean terminal.
Task Model Creation
class Task < ApplicationRecord
validates :title, presence: true, length: { minimum: 3 }
validates :status, inclusion: { in: %w[todo in_progress done] }
enum :status, { todo: 0, in_progress: 1, done: 2 }
endThe model includes strict validations and an enum for status. This ensures data integrity at the model layer before reaching the controller.
Associated Migration
class CreateTasks < ActiveRecord::Migration[8.0]
def change
create_table :tasks do |t|
t.string :title, null: false
t.text :description
t.integer :status, default: 0, null: false
t.timestamps
end
add_index :tasks, :status
end
endThe migration creates the table with an index on status to optimize filtering queries. Run rails db:migrate afterward.
Tasks Controller
class TasksController < ApplicationController
before_action :set_task, only: [:show, :update, :destroy]
def index
@tasks = Task.all
render json: @tasks
end
def show
render json: @task
end
def create
@task = Task.new(task_params)
if @task.save
render json: @task, status: :created
else
render json: { errors: @task.errors }, status: :unprocessable_entity
end
end
private
def set_task
@task = Task.find(params[:id])
end
def task_params
params.require(:task).permit(:title, :description, :status)
end
endThe controller handles standard CRUD operations with appropriate HTTP error handling. before_action factors out record lookup.
Route Configuration
Rails.application.routes.draw do
resources :tasks, only: [:index, :show, :create, :update, :destroy]
endRESTful routes are limited to required actions. This exposes only the necessary API endpoints without extra routes.
Tests with Minitest
require 'test_helper'
class TasksControllerTest < ActionDispatch::IntegrationTest
test 'should get index' do
get tasks_url
assert_response :success
assert_not_nil JSON.parse(response.body)
end
test 'should create task' do
assert_difference('Task.count') do
post tasks_url, params: { task: { title: 'Test task', status: 'todo' } }
end
assert_response :created
end
endThe tests cover the main endpoints and validate status codes. Run rails test to verify everything works correctly.
Best Practices
- Always use validations in models
- Limit parameters with strong parameters
- Return semantic HTTP status codes (201, 422, 404)
- Version the API as early as possible (/api/v1)
- Add pagination for large collections
Common Errors to Avoid
- Forgetting model validations and allowing invalid data
- Exposing sensitive attributes without a serializer
- Not handling ActiveRecord::RecordNotFound exceptions
- Ignoring integration tests for endpoints
Going Further
Explore advanced serialization with fast_jsonapi or jsonapi-serializer. Check out our advanced Rails courses.