Skip to content

Ruby on Rails - Scaffolding

When developing Rails applications, particularly those that require a straightforward interface for managing database records (CRUD operations - Create, Read, Update, Delete), scaffolding can be a powerful tool for rapid prototyping and understanding Rails conventions.

Scaffolding offers significant advantages beyond just quick demonstrations:

  • Quickly generate a functional prototype to gather early user feedback.
  • Accelerate development and achieve tangible results faster, boosting motivation.
  • Provides an excellent way to understand Rails conventions (MVC, RESTful design) by examining the generated code.
  • Serves as a solid foundation that you can customize and build upon for your specific application needs.

To illustrate scaffolding, we’ll build a simple ‘cookbook’ application to manage ‘recipes’. We’ll use Rails generators to create the necessary files and database structure for our Recipe resource.

Open your terminal or command prompt, navigate to your desired projects directory, and run the following command to create a new Rails application named ‘cookbook’:

rails new cookbook

This command creates a new Rails application directory named cookbook with a standard project structure. By default, new Rails applications (version 5.0 and later) are configured to use SQLite3 for development and testing, which is a lightweight, file-based database requiring minimal setup.

Rails handles database creation and configuration efficiently. The database settings are located in config/database.yml. For our default SQLite setup, it typically looks like this:

default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3

If you prefer to use another database like PostgreSQL or MySQL, you can specify it when creating the new application (e.g., rails new cookbook --database=postgresql). You would then need to ensure the database server is running and update config/database.yml with appropriate credentials if necessary.

Once your config/database.yml is configured (or if you’re using the SQLite default), create your development and test databases by running:

rails db:create

This command reads config/database.yml and creates the databases specified for your current environment.

The rails generate scaffold command creates a full set of Model, View, Controller (MVC) components, along with routes and a database migration file for a given resource. This provides a complete CRUD (Create, Read, Update, Delete) interface.

Navigate into your application directory (cd cookbook) and run the following command to generate scaffold for a Recipe resource. We’ll give it a title (string type) and instructions (text type):

rails generate scaffold Recipe title:string instructions:text

This command will generate several files and output a list similar to this (timestamps will vary):

invoke active_record
create db/migrate/YYYYMMDDHHMMSS_create_recipes.rb
create app/models/recipe.rb
invoke test_unit
create test/models/recipe_test.rb
create test/fixtures/recipes.yml
invoke resource_route
route resources :recipes
invoke scaffold_controller
create app/controllers/recipes_controller.rb
invoke erb
create app/views/recipes
create app/views/recipes/index.html.erb
create app/views/recipes/edit.html.erb
create app/views/recipes/show.html.erb
create app/views/recipes/new.html.erb
create app/views/recipes/_form.html.erb
create app/views/recipes/_recipe.html.erb
invoke test_unit
create test/controllers/recipes_controller_test.rb
create test/system/recipes_test.rb
invoke helper
create app/helpers/recipes_helper.rb
invoke test_unit
invoke jbuilder
create app/views/recipes/index.json.jbuilder
create app/views/recipes/show.json.jbuilder
create app/views/recipes/_recipe.json.jbuilder

Key generated components include:

  • Migration file: db/migrate/..._create_recipes.rb - Defines the schema for the recipes table in the database.
  • Model: app/models/recipe.rb - Represents a recipe. It inherits from ApplicationRecord and is used to interact with the recipes table.
  • Controller: app/controllers/recipes_controller.rb - Handles web requests, contains logic for CRUD actions on recipes.
  • Views: Files in app/views/recipes/ - ERB templates used to display recipe data and forms to the user.
  • Routes: Adds resources :recipes to config/routes.rb - Maps URLs (e.g., /recipes, /recipes/new) to controller actions according to RESTful conventions.
  • Helpers: app/helpers/recipes_helper.rb - Contains helper methods available to recipe views.
  • Tests: Various test files for the model, controller, and system interactions.
  • Jbuilder views: Files in app/views/recipes/ ending in .json.jbuilder for crafting JSON responses, useful for APIs.

Before we can use our new Recipe resource, we need to run the database migration to create the recipes table in the database according to the schema defined in the migration file:

rails db:migrate

The Generated Controller: recipes_controller.rb

Section titled “The Generated Controller: recipes_controller.rb”

Let’s examine the generated controller at app/controllers/recipes_controller.rb. It implements standard RESTful actions:

class RecipesController < ApplicationController
before_action :set_recipe, only: [:show, :edit, :update, :destroy]
# GET /recipes
# GET /recipes.json
def index
@recipes = Recipe.all
end
# GET /recipes/1
# GET /recipes/1.json
def show
# @recipe is set by the set_recipe before_action
end
# GET /recipes/new
def new
@recipe = Recipe.new
end
# GET /recipes/1/edit
def edit
# @recipe is set by the set_recipe before_action
end
# POST /recipes
# POST /recipes.json
def create
@recipe = Recipe.new(recipe_params)
respond_to do |format|
if @recipe.save
format.html { redirect_to @recipe, notice: 'Recipe was successfully created.' }
format.json { render :show, status: :created, location: @recipe }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @recipe.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /recipes/1
# PATCH/PUT /recipes/1.json
def update
respond_to do |format|
if @recipe.update(recipe_params)
format.html { redirect_to @recipe, notice: 'Recipe was successfully updated.' }
format.json { render :show, status: :ok, location: @recipe }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @recipe.errors, status: :unprocessable_entity }
end
end
end
# DELETE /recipes/1
# DELETE /recipes/1.json
def destroy
@recipe.destroy
respond_to do |format|
format.html { redirect_to recipes_url, notice: 'Recipe was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_recipe
@recipe = Recipe.find(params[:id])
end
# Only allow a list of trusted parameters through.
# This uses Strong Parameters to prevent mass assignment vulnerabilities.
def recipe_params
params.require(:recipe).permit(:title, :instructions)
end
end

Key aspects of the controller:

  • before_action :set_recipe: This callback calls the set_recipe method before the show, edit, update, and destroy actions to load the relevant @recipe instance variable.
  • RESTful Actions: index, show, new, edit, create, update, destroy correspond to standard CRUD operations and HTTP verbs.
  • respond_to: This block allows the controller to respond differently based on the request format (e.g., HTML for browsers, JSON for APIs).
  • recipe_params: This private method implements Strong Parameters, a security feature that requires you to explicitly permit which attributes can be updated through mass assignment. This is crucial for protecting your application.

When a user navigates to a URL like /recipes/1, Rails routes the request to the show action in RecipesController. The controller (via set_recipe) finds the recipe and, by convention, Rails then renders the app/views/recipes/show.html.erb template. This ‘convention over configuration’ principle simplifies development.

The controller uses Active Record methods like all, find, new, save, update, and destroy to interact with the database. Active Record is an ORM (Object-Relational Mapper) that abstracts away most SQL, allowing you to work with database records as Ruby objects.

The rails generate scaffold command, followed by rails db:migrate, quickly establishes a fully functional interface for your Recipe resource, enabling:

  • Creating new recipes
  • Editing existing recipes
  • Viewing details of a specific recipe
  • Listing all recipes
  • Deleting recipes

Scaffolding automatically generates forms for creating and editing records using Rails form helpers (like form_with). These helpers intelligently create appropriate input fields based on the model’s attribute types, such as:

  • Simple text strings (<input type='text'>)
  • Text areas (<textarea>) for larger blocks of text
  • Date selectors
  • Datetime selectors
  • Number fields
  • Checkboxes for boolean values

Rails form helpers simplify form creation and handle data submission securely, including CSRF protection.

Now that we’ve generated the scaffold and migrated the database, let’s run the Rails development server. In your cookbook directory, execute:

rails server

You might also see bin/dev used in newer Rails applications that use Procfile.dev (e.g., with jsbundling-rails or cssbundling-rails) to start the web server and other processes. rails server (or rails s) is the direct command to start the Puma web server.

Open your web browser and navigate to http://localhost:3000/recipes. You should see a page listing recipes (initially empty). Click on ‘New Recipe’ (or go to http://localhost:3000/recipes/new) to see the form for creating a new recipe.

After filling out the form (e.g., Title: ‘Chocolate Cake’, Instructions: ‘Mix ingredients and bake.’) and clicking ‘Create Recipe’, the data will be saved to the database. You’ll be redirected to the recipe’s show page, typically http://localhost:3000/recipes/1 (if it’s the first recipe).

On this page, and on the main recipes list page (http://localhost:3000/recipes), you’ll find links to ‘Show’, ‘Edit’, and ‘Destroy’ each recipe. Experiment with these CRUD operations to see the scaffold in action.

Scaffolding provides a basic model. We can enhance it by adding validations. Active Record validations help ensure data integrity before it’s saved to the database. Let’s add some rules to our Recipe model.

Open app/models/recipe.rb. By default, models in newer Rails versions inherit from ApplicationRecord. Modify it as follows:

class Recipe < ApplicationRecord
validates :title, presence: true, length: { minimum: 3, maximum: 100 }
validates :title, uniqueness: { message: "already exists. Please choose a different title." }
validates :instructions, presence: true, length: { minimum: 10 }
end

Explanation of these validations:

  • validates :title, presence: true ensures the title field is not blank.
  • validates :title, length: { minimum: 3, maximum: 100 } ensures the title is between 3 and 100 characters long.
  • validates :title, uniqueness: { message: "..." } ensures each recipe has a unique title, providing a custom error message if a duplicate is attempted.
  • validates :instructions, presence: true, length: { minimum: 10 } ensures instructions are provided and are at least 10 characters long.

Now, if you try to create or update a recipe that violates these rules (e.g., a blank title, or instructions that are too short), Rails will prevent saving and the scaffolded forms will display user-friendly error messages.

Scaffolding also generates a complete set of views (ERB templates) in the app/views/recipes/ directory. These include:

  • index.html.erb: Displays a list of all recipes.
  • show.html.erb: Displays a single recipe’s details.
  • new.html.erb: Renders the form for creating a new recipe (often using the _form.html.erb partial).
  • edit.html.erb: Renders the form for editing an existing recipe (also often using the _form.html.erb partial).
  • _form.html.erb: A partial view containing the common form fields for new and edit actions. This promotes the DRY (Don’t Repeat Yourself) principle.
  • _recipe.html.erb: A partial view for rendering a single recipe object. This is often used by the index.html.erb to render each recipe in the list and potentially by show.html.erb.

You are encouraged to explore these files to see how Rails helpers (like form_with, link_to, button_to) and ERB templating are used to build HTML dynamically. These files are your starting point and can be fully customized.

If you were to build these CRUD functionalities manually, you would need to create the controller actions, routes, model (with attributes), migrations, and view templates piece by piece. Scaffolding automates this entire process, providing a fully functional starting point instantly. This is invaluable for:

  • Rapid Prototyping: Quickly build out features for demonstration or initial user testing.
  • Learning Rails: Understand how different components of MVC interact and how Rails conventions are applied.
  • Simple Resource Management: For straightforward data models, scaffolding can provide much of the needed functionality out-of-the-box.

However, scaffolded code is just a starting point. For complex applications or features with unique logic, you’ll often need to significantly customize or even replace parts of the generated code to fit your specific requirements. Don’t be afraid to modify it!

Imagine you’re building a simple project management tool. You could scaffold a Task resource with attributes like name:string description:text due_date:date completed:boolean. This would instantly give you a web interface to create, view, update, and delete tasks, allowing you to focus on adding more advanced features next.