Web Back-End Basics: Start Your Development Tutorial

Introduction

With over 12 years building back ends, I’ll focus on the practical essentials you need to build reliable, maintainable server-side applications. This tutorial walks through core back-end concepts using Ruby on Rails 7 and PostgreSQL, showing how to build a simple task manager while covering APIs, background jobs, security for credentials, and testing.

Why Choose Ruby on Rails?

Ruby on Rails is designed for developer productivity and convention over configuration. Rails 7 emphasizes minimal JavaScript boilerplate with Hotwire/Turbo and pairs well with server-rendered or API-first architectures. Rails provides sensible defaults, a mature ecosystem of gems (authentication, authorization, background processing, testing), and built-in security features (CSRF protection, strong parameters, encrypted credentials). For teams that want fast iteration, clear conventions, and a large community, Rails remains a strong choice.

Key technical items referenced in this guide: Ruby 3.2.2, Rails 7, PostgreSQL, Sidekiq (background jobs), and RSpec for testing. These components are widely adopted and have production-grade tooling for monitoring and scaling.

Prerequisites

Before you start, ensure you have the following tools and knowledge:

  • A text editor (e.g., Visual Studio Code, Sublime Text)
  • Basic command-line interface knowledge
  • Git for version control
  • Familiarity with object-oriented programming concepts and basic data structures and algorithms

Why Use a Ruby Version Manager

Using a Ruby version manager such as rbenv or rvm prevents conflicts between system Ruby and project-specific Ruby versions. Version managers let you install multiple Ruby versions side-by-side (for example, Ruby 3.2.2 used in this guide), set per-project Ruby versions via .ruby-version, and avoid permission issues that often arise with system-wide gem installs. For reproducible development and CI parity, prefer a version manager over installing Ruby directly on the system.

Setting Up Your Development Environment

To get started with Ruby on Rails 7, set up your environment with a few recommended tools and explicit references to official sources:

  1. Install Ruby: Use a version manager like rbenv to install the correct Ruby version (example used here: Ruby 3.2.2). Version managers allow easy switching between project Rubies and avoid system-level conflicts. Check the official Ruby language site for details: ruby-lang.org.

    rbenv install 3.2.2
    rbenv global 3.2.2
    

  2. Install Rails: After Ruby is configured, install Rails 7. Confirm Rails guidance on the official site: rubyonrails.org.
    gem install rails
    
  3. Set up PostgreSQL: Ensure PostgreSQL is installed and running (PostgreSQL is widely used in production). Install via your OS package manager or from postgresql.org. Create a database user and development database. Note: create the DB owned by the rails_user to avoid permission mismatches between the DB owner and your application user.
    # For Ubuntu/Linux
    sudo apt install postgresql
    
    # For macOS
    brew install postgresql
    
    createuser --pwprompt -s rails_user
    createdb -O rails_user task_manager_development
    

Security note: Use environment variables for sensitive credentials (see Configuring Database Credentials below). Follow least privilege for DB users and use strong passwords.

Once your environment is set up, start the Rails server with rails server and begin development.

Configuring Database Credentials

Store database credentials outside of source control and reference them from config/database.yml using environment variables. A common approach is to use the dotenv-rails gem in development to load a local .env file and use platform-managed environment variables (or secrets manager) in production.

# Gemfile (development)
gem 'dotenv-rails', groups: [:development, :test]
bundle install

Note: dotenv-rails is typically used only in development and test environments to load a local .env file into ENV for convenience. In production, prefer platform-managed secrets and environment variables provided by your orchestrator or cloud provider (for example, Heroku config vars, AWS Secrets Manager, Google Secret Manager, or Kubernetes Secrets). Do not commit .env to source control; use rotation, least-privilege access, and IAM-based secret access where possible.

Example .env (DO NOT commit to Git):

# .env
DATABASE_USERNAME=rails_user
DATABASE_PASSWORD=supersecretpassword

Example config/database.yml using ENV lookups:

# config/database.yml
development:
  adapter: postgresql
  encoding: unicode
  database: task_manager_development
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: localhost
  pool: 5

In CI and production, inject these environment variables via your CI provider or cloud platform (Heroku, AWS Secrets Manager, etc.). This directly links to the earlier security recommendation to avoid hardcoding credentials.

Creating Your First Rails Application

To create a simple task manager application, run the following command:

rails new task_manager --database=postgresql
cd task_manager

This initializes a new Rails application named task_manager with PostgreSQL. Generate a Task model to manage tasks:

rails generate model Task title:string completed:boolean
rails db:migrate

Add initial data using db/seeds.rb to populate sample records:

# db/seeds.rb
Task.create(title: 'Sample Task', completed: false)
rails db:seed

This command runs the db/seeds.rb file to populate your database with initial data, useful for development and testing.

Understanding Rails and PostgreSQL

Rails provides ActiveRecord as an ORM to interact with PostgreSQL for CRUD operations. Migrations version-control schema changes and let teams evolve the schema safely.

# Creating a new task using ActiveRecord
Task.create(title: 'New Task', completed: false)

To return JSON from Rails, you can call:

# Retrieving all tasks
Task.all.to_json

For production-ready APIs, use serializers like ActiveModelSerializers or Jbuilder to control field exposure, nested relationships, and performance.

Implementing RESTful APIs

RESTful APIs enable stateless, predictable communication between clients and your server. Define resourceful routes for tasks:

# config/routes.rb
Rails.application.routes.draw do
  resources :tasks
end

Common HTTP verb to controller action mappings:

HTTP Verb Controller Action
GET index, show
POST create
PUT/PATCH update
DELETE destroy

Create a controller to handle these actions (error handling implemented inline):

# app/controllers/tasks_controller.rb
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: @task.errors, status: :unprocessable_entity
    end
  end

  def update
    if @task.update(task_params)
      render json: @task
    else
      render json: @task.errors, status: :unprocessable_entity
    end
  end

  def destroy
    @task.destroy
    head :no_content
  end

  private

  def set_task
    @task = Task.find(params[:id])
  rescue ActiveRecord::RecordNotFound # Handle cases where a task is not found, returning a 404
    render json: { error: 'Task not found' }, status: :not_found
  end

  def task_params
    params.require(:task).permit(:title, :completed)  # Strong parameters for security
  end
end

The rescue ActiveRecord::RecordNotFound in set_task captures the exception raised by Task.find when the record with the given ID does not exist. Instead of letting an uncaught exception produce a 500 response and leak a stack trace, the rescue block returns a controlled JSON error payload with a 404 status. This pattern ensures predictable API errors and avoids exposing internal implementation details to clients.

Design advice: version APIs (e.g., /api/v1/tasks) or use header-based versioning, provide consistent error payloads, document response formats, and consider rate limiting for public APIs.

Background Jobs with Sidekiq

For background processing, Sidekiq is a common choice. Use it for sending emails, processing attachments, or other long-running work. Add Sidekiq to your Gemfile and run bundle install:

gem 'sidekiq'
bundle install

Sidekiq requires Redis as a persistent job store. Install Redis on your development machine or provision a managed Redis in production. Example installation commands:

# Ubuntu/Linux
sudo apt install redis-server

# macOS
brew install redis

# Start Redis (systemd)
sudo systemctl enable --now redis-server

# Or on macOS
brew services start redis

Example ActiveJob wrapper (enqueue with perform_later):

# app/jobs/task_reminder_job.rb
class TaskReminderJob < ApplicationJob
  queue_as :default

  def perform(task)
    # Replace this with actual job logic, such as sending an email
    puts "Sending reminder for task: #{task.title}"
  end
end

Enqueue from the controller after create:

# In app/controllers/tasks_controller.rb
def create
  @task = Task.new(task_params)
  if @task.save
    TaskReminderJob.perform_later(@task)
    render json: @task, status: :created
  else
    render json: @task.errors, status: :unprocessable_entity
  end
end

Start Sidekiq locally with:

bundle exec sidekiq

Basic production-side configuration belongs in config/initializers/sidekiq.rb. Configure the Redis URL and tune concurrency from ENV variables so the same config works across environments:

# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
  # Set concurrency via environment variable; default to 10 if not provided
  Sidekiq.options[:concurrency] = Integer(ENV.fetch('SIDEKIQ_CONCURRENCY', 10))
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
end

Monitor jobs using Sidekiq's Web UI and configure retries, dead queues, and alerts to handle failures in production. In production, run Sidekiq as a supervised service (systemd, Kubernetes, or your platform's process manager) and integrate alerting for job failures or queue growth. Secure job input and any third-party integrations by validating payloads and using encrypted credentials for API keys.

Best Practices for Back-End Development

Key practices to make your back-end robust and maintainable:

  • Understand MVC: Separate concerns between Models, Views, and Controllers.
  • Authentication & authorization: Use Devise (authentication) and Pundit or CanCanCan (authorization) for role-based access control.
  • Database design: Model relations correctly, add indexes on frequently queried fields, and use migrations for schema changes.
  • Caching: Use Redis for fragment or request caching to lower DB load.
  • Monitoring: Use APM tools and structured logging to identify bottlenecks and errors.
Pro Tip: Choose REST for straightforward CRUD APIs; consider GraphQL when clients require flexible, nested queries and you want to reduce over-fetching.

Testing Your Application

Testing ensures your app behaves as expected. Use unit tests for models, request specs for controllers, and integration tests for end-to-end scenarios. RSpec is a popular framework in Rails ecosystems.

gem install rspec-rails
rails generate rspec:install

Example model spec:

# spec/models/task_spec.rb
require 'rails_helper'

RSpec.describe Task, type: :model do
  it 'is valid with valid attributes' do
    task = Task.new(title: 'Test Task', completed: false)
    expect(task).to be_valid
  end

  it 'is not valid without a title' do
    task = Task.new(title: nil)
    expect(task).to_not be_valid
  end
end

Example request spec for the API:

# spec/requests/tasks_spec.rb
require 'rails_helper'

RSpec.describe "Tasks API", type: :request do
  describe "GET /tasks" do
    it "returns a list of tasks" do
      get tasks_path
      expect(response).to have_http_status(:success)
    end
  end
end

Manual API Testing (curl & Postman)

Complement automated tests with quick manual checks using curl or Postman to validate endpoints and headers. Example curl commands against the local server:

# List tasks
curl -i http://localhost:3000/tasks

# Create a task (JSON payload)
curl -i -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"task": {"title":"Buy milk","completed":false}}'

# Update a task (id=1)
curl -i -X PATCH http://localhost:3000/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"task": {"completed":true}}'

For authenticated or more complex requests, use Postman (postman.com) to organize requests, set environment variables, inspect headers, and save example responses for documentation.

Troubleshooting tips: enable verbose logging in Rails (config.log_level = :debug in development), inspect request/response bodies, and confirm the JSON structure matches the controller's strong parameters.

Common Debugging Tips

Debugging effectively saves time during development and troubleshooting:

  • Use byebug or pry to set breakpoints and inspect runtime state.
  • Tail logs (tail -f log/development.log) to follow requests and stack traces.
  • Use DB tools like pgAdmin or the psql CLI to inspect schema and query plans.
  • Implement structured error handling and centralized exception reporting in production to capture unexpected errors.

Next Steps

As you progress, explore these advanced topics with examples and practical guidance:

  • Advanced ActiveRecord patterns: scopes, polymorphic associations, counter caches, and query optimization.
  • N+1 query detection & solutions: use .includes and .preload to eager-load associations and reduce queries.
  • Formal API versioning strategies and backward compatibility planning.
  • Deployment: containerization (Docker), buildpacks, and cloud deployment patterns (Heroku, AWS ECS/EKS, or managed platforms).
  • GraphQL for complex, client-driven queries where appropriate.
  • Observability: integrate Sidekiq monitoring, Prometheus metrics, and alerting to track background job health and queue growth.

# N+1 problem: causes one query per task to load comments
tasks = Task.all
tasks.each do |task|
  puts task.comments.count
end

# Solution: eager-load comments to perform a single additional query
tasks = Task.includes(:comments).all
tasks.each do |task|
  puts task.comments.count
end

When optimizing queries or schema, always measure using EXPLAIN/ANALYZE and profiling tools before making schema changes. Use feature flags and canary deployments to reduce risk for production changes.

Advanced ActiveRecord Patterns

Practical patterns that commonly appear in medium-to-large Rails apps:

  • Scopes for composable queries: Use named scopes to keep query logic readable and chainable.

# app/models/task.rb
class Task < ApplicationRecord
  scope :incomplete, -> { where(completed: false) }
  scope :recent, -> { order(created_at: :desc) }
end

# Usage
Task.incomplete.recent.limit(10)

Polymorphic associations: Store comments, attachments, or events that can belong to multiple models without duplicating tables.

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# app/models/task.rb
class Task < ApplicationRecord
  has_many :comments, as: :commentable
end

Counter caches and denormalization: Use counter caches to avoid expensive COUNT(*) queries, and denormalize read-heavy aggregates with background jobs to keep writes fast.

# migration
add_column :tasks, :comments_count, :integer, default: 0, null: false

# app/models/comment.rb
belongs_to :task, counter_cache: true

When applying these patterns, always add tests covering the expected behavior, guard against race conditions (use transactions where needed), and benchmark reads/writes before and after changes.

API Versioning Strategies

Choose a versioning strategy early to avoid breaking clients. Common approaches:

  • URL versioning (recommended for clarity): e.g., /api/v1/tasks — easy to route and test, explicit for clients.
  • Header-based versioning: Use an Accept header (e.g., Accept: application/vnd.app.v1+json) to keep URLs clean, but requires more client discipline and documentation.
  • Media-type versioning + feature flags: Use feature flags to roll out changes gradually while keeping backward compatibility.

Example folder layout for URL versioning:

# config/routes.rb
namespace :api do
  namespace :v1 do
    resources :tasks
  end
end

# app/controllers/api/v1/tasks_controller.rb
module Api
  module V1
    class TasksController < Api::V1::BaseController
      # actions...
    end
  end
end

When deprecating an API version, publish a clear deprecation schedule, provide migration guides, and maintain compatibility layers (serializers or presenters) to minimize client breakage.

Pro Tips from the Field

From a dozen years of shipping back-end systems, here are concrete, actionable scenarios and lessons learned—practical war stories and how to avoid common pitfalls.

  • Rolling schema changes on large tables: When adding an index to a 100M-row table in PostgreSQL, avoid blocking writes. Use CREATE INDEX CONCURRENTLY and deploy application changes in separate deploys so the migration and code that uses the index are decoupled. Run migrations during low-traffic windows and monitor pg_stat_activity to detect long-running queries.
  • Avoiding downtime for data migrations: For column backfills or expensive writes, use batched background jobs (Sidekiq) to process rows in small chunks. Use optimistic batch sizes, backoff on failures, and track progress in a migration table to allow safe restarts.
  • Sidekiq operations and reliability: Run Sidekiq with a dedicated Redis instance (namespace your keys), set concurrency according to your CPU/memory and Redis limits, and supervise Sidekiq with systemd or Kubernetes Deployments. Monitor queue depth and tune retries and dead-letter queues. If jobs suddenly back up, check Redis memory, Sidekiq logs, and recent deploys.
  • Credentials and secrets: Use Rails encrypted credentials in production (rails credentials:edit --environment production) or a cloud secrets manager. Ensure config.require_master_key = true in production so builds fail fast if keys are missing. Rotate API keys and store minimal privileges for service accounts.
  • Performance profiling: Use selective profiling—start with a slow endpoint, capture a flamegraph (APM or rack-mini-profiler in staging), and pinpoint hot SQL queries or Ruby hotspots. Add missing indexes only after verifying with EXPLAIN ANALYZE.
  • Testing and CI: Keep test suites fast by isolating slow integration tests into separate CI jobs. Use transactional fixtures or database_cleaner patterns and run a parallel test runner where possible to keep feedback loops short.

Troubleshooting checklist (quick): tail logs, reproduce locally with the same DB seed, add a breakpoint (byebug).

ActiveRecord::Base.logger = Logger.new(STDOUT)

Inspect SQL with the logger above for detailed SQL traces.

These operational practices reduce risk and speed recovery—apply them iteratively and prioritize observability, automated testing, and safe rollout strategies as your application scales.

Further Reading

Official and authoritative resources to deepen your Rails back-end knowledge:

Use these sites as starting points for official docs, tutorials, and upgrade guidance.

Key Takeaways

  • Use a Ruby version manager (rbenv/rvm) for reproducible development (example uses Ruby 3.2.2).
  • Keep credentials out of source control—use ENV and platform-managed secrets in production.
  • Design RESTful, versioned APIs and use serializers to control JSON surface area.
  • Run long-running tasks with Sidekiq and supervise it in production (systemd or container orchestrator).
  • Write tests with RSpec (unit, request, integration) and combine automated tests with manual checks (curl/Postman).
  • Detect and fix N+1 queries by eager-loading associations with .includes or .preload.

Conclusion

Understanding MVC, secure credential management, RESTful API design, background processing, and testing will allow you to build production-ready Rails applications. Use the practices shown here to create a maintainable task manager and evolve your architecture as needs grow.

About the Author

David Martinez

David Martinez is a Ruby on Rails Architect with over 12 years of experience specializing in Ruby, Rails 7, RSpec, Sidekiq, PostgreSQL, and RESTful API design. He focuses on practical, production-ready solutions and has worked on various projects, often providing insights into advanced techniques and common pitfalls in back-end development.


Published: Aug 10, 2025 | Updated: Jan 03, 2026