Build a URL Shortener in Ruby on Rails

Build a Rails app that shortens URLs and tracks click analytics.

Introduction

30 Second Summary

Every day you come across URLs that are absurdly long. You copy one to share with a friend and it takes up half the message. URL shorteners solve this by turning lengthy web addresses into tiny, memorable links.

In this project, you will build your own URL shortener from scratch using Ruby on Rails. Your app will generate unique short codes, redirect visitors to the original URL, and track how many times each link gets clicked.

What You'll Build

A full-stack web application powered by Rails that shortens URLs, handles redirects, and displays click analytics for each link.

By the end of this project, you'll have:

  • A Rails application running locally with a SQLite database storing original URLs, short codes, and click counts.
  • A working redirect system that increments a click counter every time someone visits a shortened link.
  • A clean web interface with forms to create short links and a dashboard showing all links with their analytics.
  • Secret Mission: Add QR code generation so users can scan a code to visit any shortened link.

Are there any prerequisites?

None. This guide walks you through installing every tool you need, including Ruby and Rails. All you need is a Mac with VS Code installed.

Set Up Ruby and Rails

Before you can build your URL shortener, you need the tools that power it. Ruby is the programming language, and Rails is the web framework that gives you a structured way to build web applications quickly.

In this step, you will install Ruby using Mise (the version manager recommended by the official Rails guides), install Rails itself, and create your project. By the end, you will have a running Rails app on your machine.

In this step, get ready to:
  • Install Ruby via the Mise version manager.
  • Install Rails and verify the installation.
  • Create and launch your Rails project.
Install Ruby via Mise

Ruby needs a version manager so you can install and switch between Ruby versions cleanly. Mise is the version manager recommended by the official Rails installation guide.

To install Mise, you first need Homebrew (the macOS package manager) and the Xcode Command Line Tools.

  • Press Cmd+Space (macOS) to open Spotlight Search.
  • Type Terminal and press Enter to open it.
  • Install the Xcode Command Line Tools by running this command:
xcode-select --install

What does this command do?

This installs Apple's developer command-line tools (compilers, headers, and utilities) that Homebrew and Ruby both need to build software from source.

A dialog box will pop up asking you to confirm the installation. Click Install and wait for it to finish (this may take a few minutes). If you see a message saying the tools are already installed, that is fine.

  • Check if Homebrew is already installed by running:
brew --version

✔️ I see a version number

Homebrew is already installed. Move on to the next command below.

ⓧ Command not found

  • Install Homebrew by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

What happens during installation?

The script will explain what it plans to do and ask you to press Enter to confirm. It may also ask for your macOS login password.

Type your password and press Enter (you will not see characters appear as you type, which is normal).

  • After installation finishes, add Homebrew to your PATH by running:
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
  • Reload your shell configuration by running:
source ~/.zshrc
  • Verify Homebrew works by running:
brew --version

You should see a version number printed in your terminal.

  • Install the system libraries that Ruby needs to compile by running:
brew install openssl@3 libyaml gmp rust

What are these libraries for?

These are low-level libraries that Ruby uses for encryption (openssl@3), configuration parsing (libyaml), large number math (gmp), and compiling Ruby's YJIT compiler (rust).

  • Install Mise by running:
curl https://mise.run | sh

What is Mise?

Mise is a version manager that lets you install and manage multiple versions of programming languages like Ruby, Node.js, and Python. It is the tool recommended by the official Rails installation guide.

  • Activate Mise in your shell by running:
echo 'eval "$(~/.local/bin/mise activate)"' >> ~/.zshrc
  • Reload your shell configuration so Mise takes effect by running:
source ~/.zshrc
  • Install Ruby by running:
mise use -g ruby@3

What does this command do?

This tells Mise to install the latest Ruby 3.x version and set it as your global default. The download and compilation may take a few minutes.

  • Verify Ruby is installed by running:
ruby --version

✔️ I see version 3.2 or higher

Ruby is installed and ready. You should see something like ruby 3.x.x in the output.

ⓧ I see an older version

Your system may be using a pre-installed version of Ruby that is too old for Rails 8.1.

  • Make sure Mise is activated by running:
mise doctor
  • If Mise reports a problem with activation, close Terminal completely and reopen it.
  • Run ruby --version again to confirm you see version 3.2 or higher.

ⓧ Command not found

Mise may not be properly activated in your shell.

  • Check if Mise is working by running:
mise doctor
  • If it shows activation issues, close your Terminal window completely and open a new one.
  • Try running mise use -g ruby@3 again, then check ruby --version.

Still not working?

Make sure the activation line was added to your shell config. Run cat ~/.zshrc and look for a line containing mise activate. If it is missing, re-run the activation command from above.

Help me get Ruby installed via Mise on macOS

Install Rails

Now that Ruby is installed, you can install the Rails framework. Rails is distributed as a Ruby gem (a package in the Ruby ecosystem).

  • Install Rails by running:
gem install rails

What does this command do?

The gem command is Ruby's package manager. This downloads and installs Rails along with all its dependencies. It may take a minute or two.

  • Verify Rails is installed by running:
rails --version

✔️ I see Rails 8.1 or higher

Rails is installed and ready. You should see Rails 8.1.0 or higher in the output.

ⓧ I see an older version

You may have a previous Rails installation taking priority.

  • Update Rails by running:
gem update rails
  • Then check rails --version again.

ⓧ Command not found

The rails command may not be on your PATH yet.

  • Close your Terminal and open a new one.
  • Run rails --version again.

Still seeing command not found?

Make sure Mise is activated (mise doctor should show no problems). Then try reinstalling with gem install rails.

Help me fix Rails not being found after gem install

Create your Rails project

With Ruby and Rails installed, you are ready to create your URL shortener project. The rails new command generates a complete application scaffold with all the folders, configuration files, and boilerplate you need.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Rails project by running:
rails new linky

What does rails new create?

This generates a folder called linky containing your entire Rails application structure. It includes folders for models, views, controllers, configuration, database files, and more.

Rails 8.1 uses SQLite as the default database and does not require Node.js or any JavaScript build tools.

  • Move into your new project folder by running:
cd linky
  • Create the database by running:
bin/rails db:create

What does db:create do?

This creates the SQLite database files that your application will use to store data. SQLite stores everything in a single file on your computer, so there is no separate database server to install.

  • Start the Rails development server by running:
bin/rails server
  • Open your web browser and go to http://localhost:3000.

You should see the Rails welcome page with the Rails logo and version information. This confirms your application is running correctly.

Don't see the welcome page?

Make sure the server is still running in your terminal. Look for output that says it is listening on http://127.0.0.1:3000.

If you see an error about a port being in use, another process is using port 3000. Close any other servers you may have running and try again.

Help me get my Rails server running on localhost:3000

  • Stop the server by pressing Ctrl+C in your terminal.

Now open the project in VS Code so you can edit files in the next steps.

  • Press Cmd+Space (macOS) to open Spotlight Search.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the linky folder, and click Open.

You should see the full Rails project structure in VS Code's Explorer sidebar, including folders like app, config, and db.

Your development environment is ready. Next up, you will create the database model that stores your shortened links.

Create the Link Model and Database

Your Rails app is running and the welcome page confirms everything is wired up correctly. Now it's time to give your app something to actually store.

Every URL shortener needs a place to keep track of original URLs, their short codes, and how many times each link has been clicked. In Rails, you define this structure using a model and a migration. The model represents your data in Ruby code, and the migration tells the database what table and columns to create.

In this step, get ready to:
  • Generate a Link model and database migration.
  • Run the migration and verify the table works in the Rails console.
  • Add validations and a callback that auto-generates unique short codes.
Generate the Link model and migration

Rails includes generators that create boilerplate files for you. The model generator creates both a Ruby model file and a database migration in one command.

  • Stop your Rails server by pressing Ctrl+C in the terminal.
  • Generate the Link model by running this command:
bin/rails generate model Link original_url:text short_code:string clicks:integer

What does this command do?

This tells Rails to create a model called Link with three database columns.

  • original_url (text) stores the full URL the user wants to shorten.
  • short_code (string) stores the unique 6-character code.
  • clicks (integer) tracks how many times the short link has been visited.
  • You should see output listing the files Rails created, including a migration file in db/migrate/ and a model file at app/models/link.rb.

The generated migration needs two tweaks before you run it. You need clicks to default to 0 for new links, and you need a unique index on short_code so no two links can share the same code.

  • In VS Code, open the migration file at db/migrate/. It will be named something like XXXXXXXXXX_create_links.rb where the X's are a timestamp.
  • Replace the entire contents of the migration file with this code:
class CreateLinks < ActiveRecord::Migration[8.1]
  def change
    create_table :links do |t|
      t.text :original_url
      t.string :short_code
      t.integer :clicks, default: 0

      t.timestamps
    end
    add_index :links, :short_code, unique: true
  end
end

What changed from the generated file?

  • default: 0 on the clicks column means new links start with zero clicks automatically.
  • add_index :links, :short_code, unique: true tells the database to enforce uniqueness at the database level. This prevents duplicate short codes even under heavy traffic.
  • Save the migration file.

Can't find the migration file?

In VS Code's Explorer sidebar, expand the db folder, then the migrate folder inside it. There should be exactly one file ending in _create_links.rb.

If you don't see a db/migrate folder at all, the generator command may not have run successfully. Help me find my migration file.

Run the migration and verify in the console

A migration file is just a set of instructions. Running it actually creates the table in your SQLite database.

  • Run the migration by entering this command:
bin/rails db:migrate
  • You should see output mentioning CreateLinks: migrating followed by CreateLinks: migrated. This confirms the links table now exists in your database.

Now verify the table works by creating a test record in the Rails console. The console lets you interact with your database using Ruby code.

  • Open the Rails console by running:
bin/rails console
  • You should see a prompt like linky(dev)> appear. This means you're inside an interactive Ruby session connected to your database.
  • Create a test link by typing this and pressing Enter:
Link.create(original_url: "https://www.example.com", short_code: "test01")
  • Verify the record was saved by typing:
Link.first
  • You should see the Link record printed out with original_url: "https://www.example.com", short_code: "test01", and clicks: 0. The clicks defaulted to 0 because of the migration setting.
  • Exit the console by typing:
exit

Seeing a "table not found" error?

Make sure you saved the migration file before running bin/rails db:migrate. If the migration already ran but with errors, you may need to run bin/rails db:reset to start fresh.

Help me fix my migration error.

Add validations and the short code callback

Right now the model is an empty shell. You need it to do two things: reject links that are missing a URL, and auto-generate a unique short code every time a new link is created.

Active Record callbacks let you hook into the lifecycle of a record. A before_create callback runs just before a new record is saved for the first time.

  • In VS Code, open app/models/link.rb.
  • Replace the entire contents of the file with this code:
class Link < ApplicationRecord
  validates :original_url, presence: true
  validates :short_code, uniqueness: true

  before_create :generate_short_code

  private

  def generate_short_code
    loop do
      self.short_code = SecureRandom.alphanumeric(6)
      break unless Link.exists?(short_code: short_code)
    end
  end
end

What does this code do?

  • validates :original_url, presence: true ensures every link has a URL. Rails will reject the record without one.
  • validates :short_code, uniqueness: true prevents duplicate short codes at the application level.
  • before_create :generate_short_code tells Rails to run the generate_short_code method automatically before saving a new link.
  • The loop keeps generating random 6-character codes until it finds one that doesn't already exist in the database.
  • Save app/models/link.rb.
  • Verify the callback works by opening the Rails console again:
bin/rails console
  • Create a link without specifying a short code:
Link.create(original_url: "https://www.nextwork.org")
  • You should see the record printed with a randomly generated short_code like "aB3xZ9" (yours will be different). This confirms the before_create callback is working.
  • Exit the console:
exit

Short code is nil?

If the short_code shows as nil, make sure you saved app/models/link.rb before opening the console. The console loads code fresh each time it starts.

Also check that before_create :generate_short_code is placed above the private keyword. Methods below private are still callable as callbacks, but the callback declaration itself must be in the public section.

Help me debug my short code callback.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here's the complete migration file:

class CreateLinks < ActiveRecord::Migration[8.1]
  def change
    create_table :links do |t|
      t.text :original_url
      t.string :short_code
      t.integer :clicks, default: 0

      t.timestamps
    end
    add_index :links, :short_code, unique: true
  end
end

And here's the complete model file:

class Link < ApplicationRecord
  validates :original_url, presence: true
  validates :short_code, uniqueness: true

  before_create :generate_short_code

  private

  def generate_short_code
    loop do
      self.short_code = SecureRandom.alphanumeric(6)
      break unless Link.exists?(short_code: short_code)
    end
  end
end

Your database is ready and your model knows how to generate unique short codes. Next up, you'll build the routes, controller, and views that let users actually interact with your URL shortener through the browser.

Build Routes, Controller, and Views

In the previous step, you created the Link model and database table that stores your shortened URLs. Now it's time to connect everything to the web so users can interact with your app through a browser.

Rails uses the MVC pattern to organize web applications. Routes map incoming URLs to controller actions, controllers handle the request logic, and views render the HTML. In this step, you'll build all three layers.

In this step, get ready to:
  • Define routes that map URLs to controller actions.
  • Create a controller with actions for listing, creating, showing, and redirecting links.
  • Build view templates that display forms and link data to users.
Define resourceful routes

Every Rails app has a file called config/routes.rb that acts as a map. When a browser sends a request to your app, Rails checks this file to decide which controller action should handle it.

  • In VS Code, open config/routes.rb from the Explorer sidebar.
  • Select all the existing content and replace it with the following code:
Rails.application.routes.draw do
  # Set the homepage to the links index page
  root "links#index"

  # Generate standard routes for creating and viewing links
  resources :links, only: [:index, :show, :new, :create]

  # Custom route that catches short codes and redirects
  get "/:short_code", to: "links#redirect", as: :short_redirect
end

What does this code do?

This file defines three sets of routes for your app:

  • root "links#index" makes the links index page your homepage at http://localhost:3000.
  • resources :links, only: [...] generates four standard routes: listing all links, showing one link, displaying the new-link form, and saving a new link.
  • get "/:short_code" is a custom route that catches any URL like /abc123 and sends it to the redirect action. This is the route that makes the actual URL shortening work.

Why does the custom route go last?

Rails matches routes in the order they are defined. The /:short_code pattern is very broad and would match /links or /anything if it were first.

By placing it after the resources line, Rails checks the specific link routes first and only falls through to the short code route if nothing else matches.

  • Save config/routes.rb.

You won't see a change in the browser yet. The routes are defined, but the controller they point to doesn't exist.

Create the LinksController

A controller is a Ruby class that receives requests from the router and decides what to do. Each public method in the controller is called an "action" and handles one type of request.

  • In the Explorer sidebar, right-click the app/controllers folder.
  • Click New File.
  • Name the file links_controller.rb and press Enter.
  • Paste the following code into the new file:
class LinksController < ApplicationController
  def index
    # Fetch all links, newest first
    @links = Link.all.order(created_at: :desc)
  end

  def show
    # Find a specific link by its database ID
    @link = Link.find(params[:id])
  end

  def new
    # Create an empty Link object for the form
    @link = Link.new
  end

  def create
    # Build a new link from the form data
    @link = Link.new(link_params)
    if @link.save
      redirect_to @link, notice: "Short link created!"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def redirect
    # Find the link by its short code, then track the click
    @link = Link.find_by!(short_code: params[:short_code])
    @link.increment!(:clicks)
    redirect_to @link.original_url, allow_other_host: true
  end

  private

  def link_params
    # Only allow the original_url field through for security
    params.require(:link).permit(:original_url)
  end
end

What does this controller do?

Each method handles a different user action:

  • index loads all links for the homepage listing.
  • show loads a single link's details page.
  • new prepares an empty Link object so the form knows what fields to display.
  • create saves the submitted form data. If it succeeds, the user is redirected to the show page. If validation fails, the form is re-rendered with error messages.
  • redirect is the core of the URL shortener. It finds a link by its short code, increments the click counter, then sends the visitor to the original URL.

The allow_other_host: true flag is required because Rails protects against open redirects by default. Since a URL shortener must redirect to external sites, you explicitly allow it here.

  • Save app/controllers/links_controller.rb.

You won't see a visible change yet. The controller needs view templates to render HTML for the browser.

Seeing a NameError or uninitialized constant?

Make sure your file is named exactly links_controller.rb (plural "links", all lowercase) and is saved inside app/controllers/. Rails uses naming conventions to automatically find your controller class.

Help me debug this

Create the view templates

Views are ERB templates that mix HTML with Ruby code to render dynamic pages. Each controller action automatically looks for a matching view file in a folder named after the controller.

  • In the Explorer sidebar, right-click the app/views folder.
  • Click New Folder.
  • Name the folder links and press Enter.

Now you'll create three view files inside this new app/views/links folder.

  • Right-click the app/views/links folder and click New File.
  • Name it index.html.erb and paste the following code:
<h1>Your Short Links</h1>

<%= link_to "Shorten a new URL", new_link_path, class: "button" %>

<% if @links.any? %>
  <table>
    <thead>
      <tr>
        <th>Original URL</th>
        <th>Short Code</th>
        <th>Clicks</th>
        <th>Created</th>
      </tr>
    </thead>
    <tbody>
      <% @links.each do |link| %>
        <tr>
          <td><%= truncate(link.original_url, length: 50) %></td>
          <td><%= link_to link.short_code, short_redirect_url(short_code: link.short_code) %></td>
          <td><%= link.clicks %></td>
          <td><%= link.created_at.strftime("%b %d, %Y") %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% else %>
  <p>No links yet. Create your first one!</p>
<% end %>

What does this template do?

This is the homepage of your app. It displays a table of all shortened links with their click counts.

  • <%= link_to ... %> generates an HTML anchor tag. The new_link_path helper comes from the routes you defined earlier.
  • <% @links.each do |link| %> loops through every link from the controller's @links variable and renders a table row for each one.
  • truncate shortens long URLs to 50 characters so the table stays readable.
  • Save index.html.erb.
  • If your Rails server is not running, switch to the terminal in VS Code and run bin/rails server.
  • Open http://localhost:3000 in your browser.

You should see a page titled "Your Short Links" with a "Shorten a new URL" button and a message saying "No links yet. Create your first one!"

Seeing an error instead of your page?

If you see a routing error or missing template error, check that index.html.erb is inside app/views/links/ (not directly in app/views/). The folder name must match the controller name.

Help me debug my index page

  • Create another new file in the app/views/links folder named new.html.erb and paste this code:
<h1>Shorten a URL</h1>

<%= form_with(model: @link) do |form| %>
  <% if @link.errors.any? %>
    <div class="errors">
      <h2><%= pluralize(@link.errors.count, "error") %> prevented this link from being saved:</h2>
      <ul>
        <% @link.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :original_url, "Enter a URL to shorten" %>
    <%= form.text_field :original_url, placeholder: "https://example.com/very/long/url" %>
  </div>

  <div class="actions">
    <%= form.submit "Shorten it!" %>
  </div>
<% end %>

<%= link_to "Back to all links", links_path %>

What does form_with do?

form_with(model: @link) is a Rails form helper that automatically builds an HTML form pointed at the correct URL. Since @link is a new (unsaved) Link object, Rails knows to submit the form to the create action.

The error display section at the top only appears if the form submission failed validation. It shows the user exactly what went wrong so they can fix it.

  • Save new.html.erb.
  • In your browser, click the Shorten a new URL button on the homepage.

You should see a form with a text field labelled "Enter a URL to shorten" and a "Shorten it!" submit button.

Form page not loading?

Make sure the file is named exactly new.html.erb (not new.erb or new.html). Rails expects the .html.erb double extension for HTML templates.

Help me debug my form template

  • Create one more file in the app/views/links folder named show.html.erb and paste this code:
<h1>Link Details</h1>

<div class="link-details">
  <p><strong>Original URL:</strong> <%= link_to @link.original_url, @link.original_url %></p>
  <p><strong>Short URL:</strong> <%= link_to short_redirect_url(short_code: @link.short_code), short_redirect_url(short_code: @link.short_code) %></p>
  <p><strong>Clicks:</strong> <%= @link.clicks %></p>
  <p><strong>Created:</strong> <%= @link.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
</div>

<%= link_to "Back to all links", links_path %>

What does this page display?

This is the detail page for a single shortened link. It shows the original URL, the generated short URL, how many times the link has been clicked, and when it was created.

short_redirect_url(short_code: @link.short_code) generates the full short URL (like http://localhost:3000/abc123) using the custom route you defined earlier.

  • Save show.html.erb.
  • Go back to your browser and enter a URL (like https://www.wikipedia.org) into the form.
  • Click Shorten it!.

You should be redirected to the Link Details page showing your original URL, a generated short URL, 0 clicks, and the creation date.

Seeing an error after submitting the form?

Check that your links_controller.rb has the link_params private method. Without it, Rails blocks the form data for security reasons and the create action fails.

Help me debug my create action

✔️ Awesome, I've got everything!

Great. Double check you've saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for every file you created or edited in this step.

Rails.application.routes.draw do
  root "links#index"

  resources :links, only: [:index, :show, :new, :create]
  get "/:short_code", to: "links#redirect", as: :short_redirect
end
class LinksController < ApplicationController
  def index
    @links = Link.all.order(created_at: :desc)
  end

  def show
    @link = Link.find(params[:id])
  end

  def new
    @link = Link.new
  end

  def create
    @link = Link.new(link_params)
    if @link.save
      redirect_to @link, notice: "Short link created!"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def redirect
    @link = Link.find_by!(short_code: params[:short_code])
    @link.increment!(:clicks)
    redirect_to @link.original_url, allow_other_host: true
  end

  private

  def link_params
    params.require(:link).permit(:original_url)
  end
end
<h1>Your Short Links</h1>

<%= link_to "Shorten a new URL", new_link_path, class: "button" %>

<% if @links.any? %>
  <table>
    <thead>
      <tr>
        <th>Original URL</th>
        <th>Short Code</th>
        <th>Clicks</th>
        <th>Created</th>
      </tr>
    </thead>
    <tbody>
      <% @links.each do |link| %>
        <tr>
          <td><%= truncate(link.original_url, length: 50) %></td>
          <td><%= link_to link.short_code, short_redirect_url(short_code: link.short_code) %></td>
          <td><%= link.clicks %></td>
          <td><%= link.created_at.strftime("%b %d, %Y") %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% else %>
  <p>No links yet. Create your first one!</p>
<% end %>
<h1>Shorten a URL</h1>

<%= form_with(model: @link) do |form| %>
  <% if @link.errors.any? %>
    <div class="errors">
      <h2><%= pluralize(@link.errors.count, "error") %> prevented this link from being saved:</h2>
      <ul>
        <% @link.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :original_url, "Enter a URL to shorten" %>
    <%= form.text_field :original_url, placeholder: "https://example.com/very/long/url" %>
  </div>

  <div class="actions">
    <%= form.submit "Shorten it!" %>
  </div>
<% end %>

<%= link_to "Back to all links", links_path %>
<h1>Link Details</h1>

<div class="link-details">
  <p><strong>Original URL:</strong> <%= link_to @link.original_url, @link.original_url %></p>
  <p><strong>Short URL:</strong> <%= link_to short_redirect_url(short_code: @link.short_code), short_redirect_url(short_code: @link.short_code) %></p>
  <p><strong>Clicks:</strong> <%= @link.clicks %></p>
  <p><strong>Created:</strong> <%= @link.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
</div>

<%= link_to "Back to all links", links_path %>

Your URL shortener is now fully functional. You can create short links, view their details, and the redirect system is wired up. Next up, you'll add styling to make the app look polished and test the full redirect flow end-to-end.

Style and Test Your App

Your URL shortener works, but it still uses the browser's default styling. In this step, you will add custom CSS to give the app a clean, modern look and update the layout to include proper navigation.

Once styling is in place, you will test the entire flow from end to end. Creating a short link, visiting it, and watching the click count go up proves every layer of your MVC architecture is working together.

In this step, get ready to:
  • Add CSS for a centered layout, styled table, buttons, and form inputs.
  • Update the application layout with a header and navigation links.
  • Test the full create-click-track flow end to end.
Add CSS for the page layout and table

Rails serves CSS from app/assets/stylesheets/application.css. The default file contains only a comment. You will replace it with styles that give your app a polished look.

  • In VS Code, open app/assets/stylesheets/application.css.
  • Select all the existing content and delete it.
  • Paste the following CSS to style the page body, navigation header, heading, and links table:
/* Page layout: centered content with a max width */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  background-color: #f8f9fa;
  color: #333;
}

/* Navigation bar: flexbox row with links */
header nav {
  display: flex;
  gap: 20px;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #dee2e6;
  margin-bottom: 30px;
}

/* Logo link: bold, blue, pushes other links to the right */
header nav .logo {
  font-size: 1.4rem;
  font-weight: bold;
  color: #0d6efd;
  text-decoration: none;
  margin-right: auto;
}

/* Nav links: subtle gray, no underline */
header nav a {
  color: #495057;
  text-decoration: none;
}

header nav a:hover {
  color: #0d6efd;
}

/* Page headings */
h1 {
  color: #212529;
  margin-bottom: 20px;
}

/* Links table: white card with rounded corners */
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
  background: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

th, td {
  padding: 12px 15px;
  text-align: left;
  border-bottom: 1px solid #dee2e6;
}

th {
  background-color: #e9ecef;
  font-weight: 600;
}

What does this CSS do?

The body styles center all content in an 800px column with a light gray background.

The header nav styles create a horizontal navigation bar using flexbox. The margin-right: auto on the logo pushes the other nav links to the right side.

The table styles turn the links list into a white card with rounded corners and a subtle shadow, making it feel like a real dashboard.

  • Save application.css.
  • Refresh your browser at http://localhost:3000. You should see your content centered on the page with a light gray background. If you created test links earlier, the table should have a white background with rounded corners.

No style changes showing up?

Make sure your Rails server is still running in the terminal. If you stopped it, restart it with bin/rails server.

Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to bypass the browser cache.

help me debug why my CSS changes are not showing up in the browser

Add CSS for buttons, forms, and feedback messages
  • Below the existing CSS in application.css, paste these styles for buttons, form fields, and notification messages:
/* Buttons: blue with white text */
.button, input[type="submit"] {
  display: inline-block;
  padding: 10px 20px;
  background-color: #0d6efd;
  color: white;
  text-decoration: none;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  font-size: 1rem;
}

.button:hover, input[type="submit"]:hover {
  background-color: #0b5ed7;
}

/* Form field container */
.field {
  margin-bottom: 20px;
}

.field label {
  display: block;
  margin-bottom: 5px;
  font-weight: 600;
}

.field input[type="text"] {
  width: 100%;
  padding: 10px;
  border: 1px solid #ced4da;
  border-radius: 6px;
  font-size: 1rem;
  box-sizing: border-box;
}

/* Link details card */
.link-details {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  margin-bottom: 20px;
}

/* Success notice */
.notice {
  background-color: #d1e7dd;
  color: #0f5132;
  padding: 12px 20px;
  border-radius: 6px;
  margin-bottom: 20px;
}

/* Validation error messages */
.errors {
  background-color: #f8d7da;
  color: #842029;
  padding: 12px 20px;
  border-radius: 6px;
  margin-bottom: 20px;
}

.errors ul {
  margin: 5px 0 0 0;
  padding-left: 20px;
}

What does this CSS do?

The .button and input[type="submit"] selectors style both regular links styled as buttons and form submit buttons with the same blue appearance.

The .notice class creates a green success banner. The .errors class creates a red error banner. These match the CSS classes your view templates already use.

  • Save application.css.
  • Refresh your browser. The "Shorten a new URL" link should now appear as a blue button. Click it to navigate to the new link form and confirm the input field has rounded borders with proper padding.

Button still looks like a plain link?

Check that the .button selector in your CSS exactly matches the class: "button" attribute in your index.html.erb template. Class names are case-sensitive.

help me figure out why my button CSS is not being applied

✔️ Awesome, I've got everything!

Great. Double check you have saved application.css with all the styles from both chunks above.

ⓧ I'd like to double check the full code

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  background-color: #f8f9fa;
  color: #333;
}

header nav {
  display: flex;
  gap: 20px;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #dee2e6;
  margin-bottom: 30px;
}

header nav .logo {
  font-size: 1.4rem;
  font-weight: bold;
  color: #0d6efd;
  text-decoration: none;
  margin-right: auto;
}

header nav a {
  color: #495057;
  text-decoration: none;
}

header nav a:hover {
  color: #0d6efd;
}

h1 {
  color: #212529;
  margin-bottom: 20px;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
  background: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

th, td {
  padding: 12px 15px;
  text-align: left;
  border-bottom: 1px solid #dee2e6;
}

th {
  background-color: #e9ecef;
  font-weight: 600;
}

.button, input[type="submit"] {
  display: inline-block;
  padding: 10px 20px;
  background-color: #0d6efd;
  color: white;
  text-decoration: none;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  font-size: 1rem;
}

.button:hover, input[type="submit"]:hover {
  background-color: #0b5ed7;
}

.field {
  margin-bottom: 20px;
}

.field label {
  display: block;
  margin-bottom: 5px;
  font-weight: 600;
}

.field input[type="text"] {
  width: 100%;
  padding: 10px;
  border: 1px solid #ced4da;
  border-radius: 6px;
  font-size: 1rem;
  box-sizing: border-box;
}

.link-details {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  margin-bottom: 20px;
}

.notice {
  background-color: #d1e7dd;
  color: #0f5132;
  padding: 12px 20px;
  border-radius: 6px;
  margin-bottom: 20px;
}

.errors {
  background-color: #f8d7da;
  color: #842029;
  padding: 12px 20px;
  border-radius: 6px;
  margin-bottom: 20px;
}

.errors ul {
  margin: 5px 0 0 0;
  padding-left: 20px;
}
Update the application layout with navigation

The layout file wraps every page in your app. Right now it uses the Rails default layout with no navigation. You will replace it with a custom layout that includes a header with links to the main pages.

  • In VS Code, open app/views/layouts/application.html.erb.
  • Select all the existing content and replace it with the following:
<!DOCTYPE html>
<html>
  <head>
    <title>Linky - URL Shortener</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  </head>

  <body>
    <!-- Navigation header with logo and page links -->
    <header>
      <nav>
        <%= link_to "Linky", root_path, class: "logo" %>
        <%= link_to "All Links", links_path %>
        <%= link_to "New Link", new_link_path %>
      </nav>
    </header>

    <!-- Main content area with flash notice support -->
    <main>
      <% if notice %>
        <p class="notice"><%= notice %></p>
      <% end %>
      <%= yield %>
    </main>
  </body>
</html>

What does this layout do?

The <header> contains a <nav> with three Rails link_to helpers. The first one uses class: "logo" which your CSS styles with a larger font and margin-right: auto to push it left.

The <% if notice %> block displays flash messages (like "Short link created!") whenever a controller sets one. The <%= yield %> is where each page's content gets injected.

  • Save application.html.erb.
  • Refresh your browser. You should see a navigation bar at the top with "Linky" as a bold blue logo on the left, and "All Links" and "New Link" as gray links on the right.

Navigation bar not appearing?

Make sure you replaced the entire file contents, not just appended to the bottom. The <header> block must be inside the <body> tag, before <main>.

help me fix my Rails layout so the navigation bar appears

✔️ Awesome, I've got everything!

Great. Double check you have saved application.html.erb with the new layout.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html>
  <head>
    <title>Linky - URL Shortener</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  </head>

  <body>
    <header>
      <nav>
        <%= link_to "Linky", root_path, class: "logo" %>
        <%= link_to "All Links", links_path %>
        <%= link_to "New Link", new_link_path %>
      </nav>
    </header>

    <main>
      <% if notice %>
        <p class="notice"><%= notice %></p>
      <% end %>
      <%= yield %>
    </main>
  </body>
</html>
Test the full create-click-track flow

Now that your app looks polished, it is time to prove everything works end to end. You will create a short link, visit it, and verify the click counter increments.

  • In your browser, click **New Link** in the navigation bar.
  • In the **Enter a URL to shorten** field, type https://www.wikipedia.org.
  • Click the **Shorten it!** button.
  • You should see the link details page with a green "Short link created!" notice, your original URL, a short URL, and a click count of 0.
  • Copy the short URL displayed on the page (it looks like http://localhost:3000/abc123 with a random 6-character code).
  • Paste that short URL into a new browser tab and press **Enter**.
  • Your browser should redirect you to Wikipedia.
  • Go back to your Linky app. Click **All Links** in the navigation bar.
  • Find your link in the table. The **Clicks** column should now show 1.
  • Click the short code in the table to view the link details page. The clicks count should also show 1.

What just happened?

When you visited the short URL, Rails matched the route get "/:short_code" and called the redirect action in your controller. That action found the link by its short code, called increment!(:clicks) to bump the counter in the database, then redirected your browser to the original URL.

This is the full MVC cycle in action: the route maps a URL to a controller action, the controller interacts with the model (database), and the view displays the updated data.

Click count not incrementing?

Make sure you are visiting the short URL (http://localhost:3000/abc123) and not the show page URL (http://localhost:3000/links/1). Only the short URL triggers the redirect action that increments clicks.

If you see a "No route matches" error, check that your short URL uses the exact code shown on the link details page.

help me debug why my click counter is not going up when I visit the short URL

Your URL shortener is fully styled and working end to end. Next up, you will tackle the Secret Mission and add QR code generation to make your short links even more shareable.

Secret mission

Add QR Code Generation

Your URL shortener already creates clickable short links. But what if someone needs to share a link offline, on a poster, or in a presentation? In this secret mission, you will add scannable QR codes to every shortened link using the rqrcode gem.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference or delete them entirely.

Resources you used:

  • The linky project directory on your Desktop (includes the SQLite database and all application files).
  • A local Rails development server process (if still running in your terminal).

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend your URL shortener further.

  • Your linky project directory stays on your Desktop ready for future work.
  • Mise, Ruby, and Rails remain installed on your system for future Rails projects.
  • If you stopped the Rails server, you can restart it anytime by opening a terminal in the linky folder and running bin/rails server.

Pause - I'll come back to this later

Stop the Rails server to free up your terminal. Your project files stay intact for when you return.

  • Switch to the terminal where your Rails server is running.
  • Press Ctrl+C to stop the server.
  • Your linky directory and database remain on your Desktop. When you're ready to continue, open a terminal in the folder and run bin/rails server again.

Delete - I don't want to use this again

Remove all project files and start fresh. This permanently deletes your URL shortener and its database.

  • If your Rails server is still running, press Ctrl+C to stop it.
  • Navigate to your Desktop and remove the project directory by running these commands:
cd ~/Desktop
rm -rf linky
  • Confirm the directory is gone by running:
ls | grep linky

You should see no output, confirming the folder has been deleted.

What about Mise, Ruby, and Rails?

These tools remain installed on your system. They're useful for future Rails projects and don't consume resources when idle.

If you want to remove them, run brew uninstall mise to remove Mise (which also removes the Ruby versions it manages). The Rails gem is removed along with the Mise-managed Ruby.

Nice Work!

Nice Work!

You just built a fully functional URL shortener from scratch using Ruby on Rails. Your app takes long URLs, generates unique short codes, tracks every click, and displays live analytics.

You've learned how to:

  • Build a complete Rails 8.1 application with an Active Record model that uses validations, callbacks, and business logic to auto-generate short codes.
  • Implement resourceful routing and a custom redirect route with click tracking, connecting the full MVC cycle from routes to controllers to views to models.
  • Create ERB views with form helpers, dynamic data rendering, and a styled interface backed by a SQLite database.
  • Secret Mission: Added QR code generation using the rqrcode gem so users can scan a code to visit any shortened link.

Ready to quiz yourself?