Free Programming learning guide
Master Ruby on Rails: Intermediate Web Development
Master Ruby on Rails: Intermediate Web Development — a free intermediate-level guide covering learn ruby on rails for web development. Learn with clear...
What you will learn
- Ruby Foundations for Rails
- Rails Architecture and MVC
- Active Record and Database Interactions
- Views, Helpers, and Asset Pipeline
- Forms, Parameters, and Authentication
- Advanced Routing and Controllers
- Testing with Minitest and Fixtures
- Action Mailer and Background Jobs
- API Mode and JSON Serialization
- Deployment and Production Best Practices
1. Ruby Foundations for Rails
The Ruby Object Model: More Than Just Syntax Consider a common sight in a Rails application: user.posts.create(title: "Hello World"). To a beginner, this looks like magic. To an intermediate developer, it’s a chain of method calls. But to truly understand Rails, you need to see the underlying machinery: how user finds the posts method, how posts returns an object that responds to create, and how title: "Hello World" is packaged and passed. Rails is a framework written in Ruby, which means its conventions—Active Record associations, controller filters, view helpers—are just Ruby classes, modules, and methods. To write idiomatic Rails, you must first write idiomatic Ruby. Everything is an Object (Almost) In Ruby, almost everything is an object. Integers, strings, arrays, and even classes themselves are instances of a class. This means every value responds to methods and holds state. The only exceptions are a handful of primitives like blocks and methods (when referenced without being bound to an object). Every object in Ruby is an instance of a class, and every class inherits from a single root: BasicObject. The chain usually looks like BasicObject - Object - YourClass. When you call a method on an object, Ruby looks to the right (the object's class), then up (the class's ancestors) to find the method. If it reaches BasicObject and finds nothing, it raises a NoMethodError. But before raising that error, Ruby does something special: it asks the object if it knows how to handle the missing method dynamically. Classes and Instances A class in Ruby is a blueprint. You define it using the class keyword. Inside, you define methods that will be available to instances of that class. Here, @title is an instance variable. It is scoped to the specific instance of Article and is hidden from the outside world. The title method is a basic getter. Writing getters and setters manually is tedious. Ruby provides attraccessor, attrreader, and attrwriter to generate these methods dynamically. Class Methods and Class Variables Sometimes you need methods that belong to the class itself, not to an instance. In Rails, Article.find(1) is a class method. You define class methods by prefixing the method name with self.. You will rarely see explicit class variables (@@var) in modern Rails code because they are notoriously tricky with inheritance. Instead, Rails developers typically use class instance variables (@var defined at the class level) or module-level constants. Modules: Sharing Behavior As your Rails application grows, you will find that different models share similar behaviors. For example, both User and Article might need to be tagged, or both might need to track who last updated them. Ruby solves this with modules. A module is a collection of methods and constants. Unlike …
2. Rails Architecture and MVC
The Request/Response Lifecycle Imagine a user typing https://yoursite.com/articles/42 into their browser and hitting Enter. In that fraction of a second before the page renders, Rails performs a highly choreographed sequence of events. Understanding this sequence is the key to debugging issues, structuring your application logically, and knowing exactly where to place your business logic. The Rails request/response lifecycle is a continuous loop that bridges the web server (like Puma) and your application code. When a request hits the server, it is passed to the Router. In Ruby Foundations for Rails, we built a rudimentary Router class and an HttpVerbs module to understand how Ruby handles method dispatching. The Rails router does exactly what our custom router did, but on a much grander scale. It parses the HTTP verb (GET, POST, PUT, DELETE) and the URL path, attempting to match it against a defined set of routes. When a match is found, the router instantiates the appropriate Controller and invokes the specific Action (a public method on the controller). This is where the Model comes in. The controller asks the Model for data—say, finding an article with the ID of 42. Once the Model returns the data, the controller packages it up and passes it to the View. The View renders the HTML template, combining static markup with the dynamic data provided by the controller. Finally, the assembled HTML is wrapped in an HTTP response and sent back to the user's browser. To summarize the exact flow of a standard GET request: 1. Browser sends an HTTP request to the server. 2. Router matches the URL and HTTP verb to a specific controller action. 3. Controller action executes. It may request data from the Model. 4. Model queries the database and returns Ruby objects to the controller. 5. Controller passes data to the View via instance variables. 6. View renders the HTML template. 7. Controller sends the compiled view back to the browser as the HTTP response. If any step in this chain fails—an unmatched route, a missing database record, a syntax error in the view—Rails raises an exception, interrupting the cycle and returning an error page (or a JSON error payload) to the client. Generating a New Rails Application Now that we understand the theoretical lifecycle, let's see how Rails structures the files that make it happen. To create a new application, you use the rails new command. Open your terminal and run: This command generates a massive directory structure. While it can feel overwhelming, Rails relies heavily on convention over configuration. This means you don't need to configure file paths manually; Rails automatically knows where to find things based on the folder they live in. Here is the core …
3. Active Record and Database Interactions
The Object-Relational Impedance Mismatch Relational databases organize data in tables, rows, and columns. Ruby organizes data in objects, attributes, and methods. This fundamental disconnect is known as the object-relational impedance mismatch. In a raw PHP or Ruby script, bridging this gap means writing tedious SQL queries and manually looping through result sets to hash together object instances. Active Record is Rails' solution to this problem. It is an implementation of the Active Record pattern, where an object wraps a row in a database table, encapsulates the database access, and adds domain logic to that data. If you built a Router and Controller in the previous modules to handle HTTP traffic, Active Record is the layer that sits between your controllers and your database, translating Ruby method calls into SQL queries and returning Ruby objects. Migrations: Versioning Your Schema Databases are not static. As an application evolves, you need to add tables, introduce columns, and modify data types. Doing this manually via a database GUI is risky and makes collaboration nearly impossible. Migrations provide a version-controlled, Ruby-DSL approach to altering your database schema. A migration is a subclass of ActiveRecord::Migration. When you generate a model—say, Article—Rails automatically generates a migration file prefixed with a timestamp. When you run rails db:migrate, Rails executes the change method. The createtable block yields a table definition object. The t.timestamps method automatically creates two datetime columns: createdat and updatedat, which Active Record manages for you. Reversibility and Raw SQL Migrations are designed to be reversible. If you make a mistake, running rails db:rollback undoes the last migration. Rails knows how to reverse most standard methods (like createtable becoming droptable). However, if a migration cannot be automatically reversed, or if you need to execute raw SQL, you must define up and down methods explicitly instead of change. Migrations should be treated as immutable once applied to a shared database. If you need to change an existing migration that has already been pushed to version control, write a new migration to make the alteration rather than editing the old one. Defining Models and Associations A model in Rails is a Ruby class that inherits from ApplicationRecord, which in turn inherits from ActiveRecord::Base. This inheritance chain is what injects the database interaction methods into your class. With just those two lines of code, Active Record dynamically defines getter and setter methods for every column in the articles table. If you add a column authorname to the table via a migration, the Article class instantly responds to article.authorname and article.authorname=. This relies on Ruby's methodmissing and dynamic method definition, concepts that build on the Ruby foundations covered earlier. Establishing Relationships Databases use foreign keys to link tables. Active Record uses …
4. Views, Helpers, and Asset Pipeline
Rendering Dynamic Content with ERB When a controller action finishes its work, it typically hands off a set of instance variables to the view layer. The view’s job is to transform that data into HTML. In Rails, the default tool for this transformation is ERB (Embedded Ruby). If you recall from our exploration of Rails Architecture and MVC, the controller doesn't explicitly tell the view to render by default—it just ends, and Rails conventions take over. The controller passes data down using instance variables, which act as the bridge between the controller and the view. An ERB template is HTML interspersed with Ruby code. You write Ruby inside specific tags: - <% ... % executes the Ruby code but does not inject the return value into the HTML. This is used for logic like loops and conditionals. - <%= ... % executes the Ruby code and injects the return value directly into the HTML output. Consider a scenario where an ArticlesController sets an @article instance variable, and we want to display its details along with its associated comments. Notice how we use @article.comments.each with the <% % tag to iterate without printing the array itself, while using <%= % to output the specific string attributes. Because @article is an Active Record object (as covered in the previous chapter), we can call its getter methods and associated collections directly in the template. However, embedding too much Ruby logic inside ERB quickly leads to unmaintainable templates. If you find yourself writing complex dig calls into nested hashes or performing data transformations directly in the template, it’s time to reach for helpers. Encapsulating View Logic with Helpers Helpers are Ruby modules that encapsulate view logic, keeping your ERB templates clean and focused on structure. Rails automatically makes all helpers available to all views, though you can also scope helpers to specific controllers if needed. Built-in Helpers Rails ships with a vast library of built-in helpers. These cover everything from formatting dates to generating HTML tags and links. One of the most frequently used is linkto, which generates an anchor tag: Another powerful built-in is timeagoinwords, which translates a timestamp into a human-readable relative format: Instead of writing raw HTML forms or manually concatenating strings, helpers allow you to build interfaces using Ruby methods. This is especially useful when you need to conditionally apply classes or data attributes. Creating Custom Helpers When logic gets complex or repetitive, you should extract it into a custom helper. For instance, imagine you are building a dashboard that displays a user's account status. The status is stored as an integer in the database (e.g., 0 for inactive, 1 for active, 2 for premium), and you need to display …
5. Forms, Parameters, and Authentication
Building Forms with formwith Every interaction a user has with your application—signing up, posting a comment, resetting a password—flows through an HTML form. In Chapter 4, we explored how Rails renders views, but form generation requires a deeper integration with the framework's routing and model layers. Rails provides the formwith helper to generate these forms. Unlike static HTML, formwith binds directly to Active Record objects, automatically inferring the correct URL, HTTP method, and input names based on the object's state. Consider a scenario where we are building a user registration form. If we pass a new User object to formwith, Rails inspects the object to determine where the form should submit. Because @user is a new, unpersisted record, Rails routes this POST request to the userscreate action. If @user had been retrieved from the database (e.g., User.find(params[:id])), Rails would automatically generate a hidden method field set to patch, routing the request to update instead. Notice the local: true argument. By default, formwith submits forms remotely using XHR (Ajax). Setting local: true forces a standard, full-page HTML form submission, which is often what you want for standard account registrations and logins. The generated HTML uses a specific naming convention for input fields: user[email], user[password]. When the form is submitted, Rails parses this nested structure into a nested Hash in the controller, accessible via the params object. Strong Parameters and Mass Assignment When a form submits, the params object in your controller contains all the data sent by the browser. In Chapter 3, we discussed Active Record models and how easily we can instantiate and update records using hashes: While convenient, this introduces a severe security vulnerability known as mass assignment. If a malicious user intercepts the request and adds user[admin]=true to the payload, the above code would happily grant them administrative privileges. To prevent this, Rails enforces Strong Parameters. Strong parameters require you to explicitly declare which parameters are permitted, filtering out anything unauthorized before it reaches the model. Instead of passing the raw params hash, you define a private method in your controller: Here, require(:user) ensures the user key exists in the parameters, raising an error if it's missing. permit acts as a whitelist, stripping out any keys not explicitly listed. If an attacker tries to pass admin=true, it is silently discarded by permit before User.new is ever called. Implementing Authentication from Scratch With forms safely handling input, we can tackle authentication. While gems like Devise exist, building authentication from scratch is the best way to understand how Rails manages user sessions and state. The hassecurepassword Macro Rails provides a built-in module called hassecurepassword. When added to an Active Record model, it automatically: - Adds methods to securely hash and …
6. Advanced Routing and Controllers
Imagine an e-commerce platform where a customer clicks "Refund" on a specific order. The request hits your Rails application. Where does it go? It needs to find the specific order, trigger a custom refund process, ensure the user is actually authorized to request a refund for that order, and gracefully handle any payment gateway failures. In Rails Architecture and MVC, we established the router as the traffic cop directing requests to controllers. In Forms, Parameters, and Authentication, we secured our application and handled basic data input. Now, we need to wire up the complex, nested, and custom workflows that make real-world applications tick. We will move briskly past standard RESTful routes and dive into structuring complex application flows using nested routes, custom endpoints, controller filters, and graceful exception handling. Nested and Shallow Routes When resources are logically dependent on one another, standard flat routing becomes cumbersome. If a User has many Orders, fetching a specific order requires knowing the user. We could route to /orders/:id, but this leaves the user context implicit, forcing us to dig through parameters or sessions to find the parent. Nested routes make this hierarchy explicit in the URL. To nest routes, you pass a block to the parent resource in your config/routes.rb file: This generates standard RESTful routes for orders, but prefixes them with the user. The path helper becomes userorderspath(@user), and the URL becomes /users/:userid/orders/:id. In the OrdersController, the :userid parameter is now available to scope the database query: The Problem with Deep Nesting A common mistake is nesting resources too deeply. If you nest orders under users, and items under orders, you end up with URLs like /users/:userid/orders/:orderid/items/:id. This creates long, brittle URL helpers (userorderitempath(@user, @order, @item)) and requires passing multiple IDs to find a single record. Rails provides a built-in mechanism to prevent this: shallow routes. By adding shallow: true to your parent resource, Rails generates nested routes only for actions that require the parent's context (like index, new, and create). For actions that operate on a single child record by its unique ID (show, edit, update, destroy), it flattens the route back to the top level. With shallow: true: Creating an item requires knowing the order: POST /orders/:orderid/items Viewing an item only requires the item's ID: GET /items/:id This keeps your URLs intuitive and your controller lookups simple. If an Item has a globally unique ID, you don't need the userid or orderid to fetch it. Member and Collection Routes Standard REST provides seven actions (index, show, new, create, edit, update, destroy), but business logic often demands more. When you need an action that doesn't fit neatly into CRUD, Rails gives you member routes and collection routes. Member routes apply to …
7. Testing with Minitest and Fixtures
The Anatomy of a Rails Test Every Rails application ships with a testing framework called Minitest. Because it is baked into Ruby’s standard library and integrated directly into Rails, it provides a lightweight, fast, and highly effective way to verify your application's behavior without adding heavy dependencies. When you generated your models and controllers in previous chapters, Rails automatically created corresponding test files for you. These files live in the test/ directory. Before we write our own tests, let’s look at the basic building blocks. A test in Minitest is simply a Ruby class that inherits from ActiveSupport::TestCase. Inside this class, any method that begins with test is automatically executed as a test. The core of Minitest relies on assertions—methods that check if the state of your code matches your expectations. If an assertion evaluates to false, the test fails. Here are the assertions you will use most frequently: - assert: Checks that a value is truthy. - assertequal: Checks that two values are exactly equal. - assertnil: Checks that a value is nil. - assertraises: Checks that a specific block of code raises an expected error. - assertdifference: Checks that a numeric value changes by a certain amount after executing a block. To run your test suite, you use the Rails command line: To run a specific file or even a specific test method, you can pass the file path and line number: Writing Model Tests Models are the heart of a Rails application. Because they encapsulate business logic, database interactions, and data integrity rules, they are usually the best place to start your testing journey. Building on the Active Record concepts covered earlier, we will write tests to ensure our validations and associations behave as expected. Testing Validations In Active Record, validations prevent invalid data from being saved to the database. Instead of manually checking if an object is valid, Minitest provides a custom assertion specifically for Rails: assertvalid (or conversely, checking the errors object). Imagine an Article model that requires a title and enforces a minimum length for the body. To test this, we need to verify two things: a valid article can be saved, and an invalid article cannot. Notice the second argument passed to assertnot. This optional string is printed if the test fails, acting as a helpful debugging message to your future self or your teammates. Testing Associations Associations define how your models relate to one another. If an Article hasmany Comments, you want to ensure that calling article.comments actually returns an array-like collection, and that destroying the article handles the comments appropriately. While you could manually test the SQL generated by Active Record, it is more practical to test the behavior of the …
8. Action Mailer and Background Jobs
The Request-Response Cycle is a Terrible Place for Slow Work Imagine a user signs up on your platform. You need to save their record to the database, send a welcome email, and notify an admin via a third-party Slack integration. If you do all of this inside your controller action, the user's browser will spin idly while your server waits for the SMTP server to respond and the Slack API to resolve. If the Slack API takes five seconds to time out, the user waits five seconds for a simple "Account created" response. The HTTP request-response cycle should be reserved for quick operations: parsing parameters, interacting with the database, and rendering a response. Anything slow—email delivery, API calls, heavy data processing, or generating PDFs—should be pushed outside the cycle into background jobs. In this chapter, we will decouple slow tasks from the user's request using Active Job and Action Mailer, moving from the default asynchronous behavior to a production-grade queue adapter like Sidekiq. Sending Email with Action Mailer Action Mailer is Rails' built-in framework for generating and sending emails. If you recall the MVC architecture covered in earlier chapters, Action Mailer behaves much like a controller. It receives data, assigns it to an instance variable, and renders a view template. The primary difference is that the "view" becomes an email body rather than an HTML page. Generating a Mailer You can generate a mailer just as you would a controller. Let's create a mailer to handle user registration events: This command creates a few files: - app/mailers/usermailer.rb: The mailer class. - app/views/usermailer/welcomeemail.html.erb: The HTML email template. - app/views/usermailer/welcomeemail.text.erb: The plain-text alternative. - Preparatory files for tests (building on the testing foundations from Chapter 7). Configuring the Mailer Inside the newly generated UserMailer class, you'll define a public method that prepares the email. Notice the default method, which sets default headers for all emails sent from this class. The mail method accepts parameters like :to and :subject and triggers the rendering of the views. By providing both .html.erb and .text.erb templates, Rails automatically generates a multipart/alternative email, allowing the recipient's email client to choose the best format to display. Delivering the Email With the mailer configured, you can trigger the email from anywhere in your application, such as a controller: Here, the .with method passes a hash of parameters into the mailer action, making user available as a local variable or via params[:user] inside the mailer method. You have a few delivery methods available: - deliverlater: Enqueues the email to be sent asynchronously via Active Job. This is the default and recommended approach. - delivernow: Bypasses Active Job and sends the email immediately within the current process. Active Job: Enqueuing Background …
9. API Mode and JSON Serialization
The API-Only Paradigm Imagine your team has just built a successful Rails application using server-rendered views. Now, marketing wants a native mobile app, and the design team wants to build a cutting-edge single-page application (SPA) using React. You don't need to rewrite your business logic or database schema; you just need a way to expose your data to these new clients. This is where API mode comes in. Instead of generating HTML views, your Rails application acts purely as a data server, responding to requests with JSON (JavaScript Object Notation). In Rails Architecture and MVC, we discussed how a standard Rails controller request flows through middleware, routing, controllers, and views. When you generate a standard Rails app, it includes middleware for cookies, flash messages, and session management, alongside the entire Action View library for rendering HTML. When you create an API-only application using rails new backendapp --api, Rails strips away the unnecessary bloat. It configures the application to: - Load a lighter middleware stack, omitting cookies, sessions, and flash notices. - Skip generating view templates, helpers, and asset pipeline files. - Inherit controllers from ActionController::API instead of ActionController::Base. If you are adding API endpoints to an existing monolithic application, you don't need to convert the whole app to API mode. You can simply make specific controllers inherit from ActionController::API to opt-out of the view-rendering overhead for those routes. Rendering JSON Responses Rendering JSON in Rails is remarkably straightforward due to Ruby’s native JSON module and the deep integration between Active Record and Hashes. As we saw in Ruby Foundations for Rails, you can easily convert Hashes and Arrays to JSON. Rails takes this a step further by allowing you to pass Active Record objects directly to the render method. Built-in JSON Rendering The simplest way to return JSON is using the render json: method in your controller. Rails automatically calls .tojson on the object, converting Active Record models into JSON hashes. By default, Rails will include all columns from the database table. You can restrict or expand what is returned using the :only and :include options. While only and include work for trivial use cases, they quickly become unmanageable. If your Article model has 20 columns and you only want to expose 3, listing them in every controller action violates the DRY (Don't Repeat Yourself) principle. Furthermore, you often need to compute custom attributes that don't exist in the database. Custom Serializers To solve this, Rails developers use serializers to define a strict, reusable blueprint for how a model should be converted to JSON. While there are several popular gems (like jsonapi-serializer or activemodelserializers), we can easily build a lightweight, custom serializer using plain Ruby classes. Because we already know …
10. Deployment and Production Best Practices
The Shift from Development to Production Your Rails application runs flawlessly on your local machine. You boot it with rails server, navigate to localhost:3000, and everything works. But local development is a sanctuary. Your computer has access to your local database, environment variables are often hardcoded or loaded from untracked files, and performance bottlenecks are masked by a single user making requests. Moving an application to production changes the rules entirely. A production environment must be secure, available to the public, resilient to failure, and capable of handling concurrent traffic. Preparing for this shift requires configuring environment variables, setting up a robust database, configuring a production-grade web server, and deploying to a cloud provider. Managing Secrets and Environment Variables In previous chapters, we used standard configuration for database connections and API integrations. In production, you cannot commit sensitive data—like API keys, database passwords, or mailer credentials—to version control. Rails provides two primary mechanisms for handling this: Environment Variables and Encrypted Credentials. Environment Variables Environment variables are set at the operating system or hosting platform level. They are the standard way to configure applications in modern cloud deployments because they adhere to the Twelve-Factor App methodology. In Rails, you can access environment variables anywhere via the ENV hash, which behaves like a standard Ruby Hash. While you can use ENV directly, it is generally better to use Rails credentials for application secrets, reserving environment variables for infrastructure-level configurations (like database URLs or Redis connection strings). Rails Encrypted Credentials Rails provides a built-in credentials system that encrypts sensitive data using a master key. The encrypted file (config/credentials.yml.enc) is safe to commit to version control, but the master key (config/master.key) must never be committed. To edit your credentials, run: This decrypts the file in your editor. Inside, you use YAML to structure your secrets: In your application, you access these values via the Rails.application.credentials object: When deploying to platforms like Heroku or Render, you must set the RAILSMASTERKEY environment variable on the server. Without it, Rails will crash on boot because it cannot decrypt the credentials file. Configuring the Production Database Throughout this book, we've used SQLite for development and testing. SQLite is a fantastic file-based database, but it relies on local disk storage. In modern cloud deployments, file systems are often ephemeral—meaning they are wiped clean on every deploy. For production, you need a client-server database like PostgreSQL. PostgreSQL handles concurrent connections, survives deploys, and integrates seamlessly with Active Record. Updating the Gemfile To use PostgreSQL, ensure the pg gem is in your production group. Often, Rails developers will use SQLite for development and PostgreSQL for production, though using PostgreSQL across all environments is highly recommended to match your production environment as …
Continue learning
- Intermediate Python Automation Scripts for BeginnersIntermediate Python Automation Scripts for Beginners — a free intermediate-level guide covering intermediate python automation scripts for beginners....
- Intermediate Python Projects for Portfolio BuildingIntermediate Python Projects for Portfolio Building — a free intermediate-level guide covering intermediate python projects for portfolio building....
- C# for Beginners: A Complete Step-by-Step GuideC# for Beginners: A Complete Step-by-Step Guide — a free beginner-level guide covering how to learn c# for beginners. Learn with clear explanations,...
- Advanced SQL for Data Analysts: Mastering Complex QueriesAdvanced SQL for Data Analysts: Mastering Complex Queries — a free advanced-level guide covering advanced sql queries for data analysts. Learn with...