Setup 🛠 Rails 8 App – Part 5: Active Storage File Uploads 📤

Meanwhile we are setting up some UI for our app using Tailwind CSS, I have uploaded 2 images to our product in the rich text editor. Let’s discuss about this in this post.

Understanding Active Storage in Rails 8: A Deep Dive into Image Uploads

In our Rails 8 application, we recently tested uploading two images to a product using the rich text editor. This process internally triggers several actions within Active Storage. Let’s break down what happens behind the scenes.

How Active Storage Handles Image Uploads

When an image is uploaded, Rails 8 processes it through Active Storage, creating a new blob entry and storing it in the disk service. The following request is fired:

Processing by ActiveStorage::DirectUploadsController#create as JSON

Parameters: {"blob" => {"filename" => "floral-kurtha.jpg", "content_type" => "image/jpeg", "byte_size" => 107508, "checksum" => "GgNgNxxxxxxxjdPOLw=="}}

This request initiates a database entry in active_storage_blobs:

INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "service_name", "byte_size", "checksum", "created_at")
VALUES ('huk9dxxxxxxxx09e2cyiq', 'floral-kurtha.jpg', 'image/jpeg', 'local', 107312, 'Fxxxxxxd+bpRibo2EfvA==', '2025-03-31 08:10:07.232453')

Storing Files and Generating URLs

Once the blob entry is created, Rails stores the file on disk and generates a URL:

http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsiYSI6eyJxxxxxxx

This process triggers the ActiveStorage::DiskController, handling file storage via a PUT request:

Started PUT "/rails/active_storage/disk/eyJfcmFpbHMiOxxxxx"
Disk Storage (0.9ms) Uploaded file to key: hut9d0zxssxxxxxx
Completed 204 No Content in 96ms

Retrieving Images from Active Storage

After successfully storing the file, the application fetches the image via a GET request:

Started GET "/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOxxxxxxxxxxfQ==--f9c556012577xxxxxxxxxxxxfa21/floral-kurtha-2.jpg"

This request is handled by:

Processing by ActiveStorage::Blobs::RedirectController#show as JPEG

The file is then served via the ActiveStorage::DiskController#show:

Redirected to http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsiZGxxxxxxxxxd048aae4ab5c30/floral-kurtha-2.jpg

Updating Records with Active Storage Attachments

When updating a product, the system also updates its associated images. The following Active Storage updates occur:

UPDATE "action_text_rich_texts" SET "body" = .... WHERE "action_text_rich_texts"."id" = 1

UPDATE "active_storage_blobs" SET "metadata" = '{"identified":true}' WHERE "active_storage_blobs"."id" = 3

INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES ('embeds', 'ActionText::RichText', 1, 3, '2025-03-31 11:46:13.464597')

Additionally, Rails updates the updated_at timestamp of the associated records:

UPDATE "products" SET "updated_at" = '2025-03-31 11:46:13.523640' WHERE "products"."id" = 1

Best Practices for Active Storage in Rails 8

  1. Use Direct Uploads: This improves performance by uploading files directly to cloud storage (e.g., AWS S3, Google Cloud Storage) instead of routing them through your Rails server.
  2. Attach Images Efficiently: Use has_one_attached or has_many_attached for file associations in models.
  3. Avoid Serving Files via Rails: Use a CDN or proxy service to serve images instead of relying on Rails controllers.
  4. Clean Up Unused Blobs: Regularly remove orphaned blob records using ActiveStorage::Blob.unattached.destroy_all.
  5. Optimize Image Processing: Use variants (image.variant(resize: "300x300").processed) to generate resized images efficiently.

In Rails 8, Active Storage uses two main tables for handling file uploads:

1. active_storage_blobs Table

This table stores metadata about the uploaded files but not the actual files. Each row represents a unique file (or “blob”) uploaded to Active Storage.

Columns in active_storage_blobs Table:

  • id – Unique identifier for the blob.
  • key – A unique key used to retrieve the file.
  • filename – The original name of the uploaded file.
  • content_type – The MIME type (e.g., image/jpeg, application/pdf).
  • metadata – JSON data storing additional information (e.g., width/height for images).
  • service_name – The storage service (e.g., local, amazon, google).
  • byte_size – File size in bytes.
  • checksum – A checksum to verify file integrity.
  • created_at – Timestamp when the file was uploaded.

Example Entry in active_storage_blobs:

INSERT INTO "active_storage_blobs" 
("key", "filename", "content_type", "service_name", "byte_size", "checksum", "created_at") 
VALUES ('avevnp6eg1xxxxxxsz8it6267eou7', 'floral-kurtha-2.jpg', 'image/jpeg', 'local', 204800, '0U0cXxxxxxxxxx/1u47Szg==', '2025-03-31 11:45:07.232453');

👉 Purpose: This table acts as a record of stored files and their metadata.


2. active_storage_attachments Table

This table links blobs (files) to Active Record models. Instead of storing files directly in the database, Rails stores a reference to the blob.

Columns in active_storage_attachments Table:

  • id – Unique identifier for the attachment.
  • name – Name of the attachment (:avatar, :images, etc.).
  • record_type – The model type associated with the file (User, Post, etc.).
  • record_id – The ID of the record in the model (users.id, posts.id).
  • blob_id – The corresponding ID from active_storage_blobs.
  • created_at – Timestamp when the association was created.

Example Entry in active_storage_attachments:

INSERT INTO "active_storage_attachments" 
("name", "record_type", "record_id", "blob_id", "created_at") 
VALUES ('avatar', 'User', 1, 42, '2025-03-31 08:15:20.123456');

INSERT INTO "active_storage_attachments" 
("name", "record_type", "record_id", "blob_id", "created_at") 
VALUES ('embeds', 'ActionText::RichText', 1, 4, '2025-03-31 11:46:20.123456');

👉 Purpose: This table allows a single file to be attached to multiple records without duplicating the file itself.


Why Does Rails Need Both Tables?

  1. Separation of Concerns:
    • active_storage_blobs tracks the files themselves.
    • active_storage_attachments links them to models.
  2. Efficient File Management:
    • The same file can be used in multiple places without storing it multiple times.
    • If a file is no longer attached to any record, Rails can remove it safely.
  3. Supports Different Attachments:
    • A model can have different types of attachments (avatar, cover_photo, documents).
    • A single model can have multiple files attached (has_many_attached).

Example Usage in Rails 8

class User < ApplicationRecord
  has_one_attached :avatar   # Single file
  has_many_attached :photos  # Multiple files
end

When a file is uploaded, an entry is added to active_storage_blobs, and an association is created in active_storage_attachments.

How Rails Queries These Tables

user.avatar # Fetches from `active_storage_blobs` via `active_storage_attachments`
user.photos.each { |photo| puts photo.filename } # Fetches multiple attached files

Conclusion

Rails 8 uses two tables to decouple file storage from model associations, enabling better efficiency, flexibility, and reusability. This structure allows models to reference files without duplicating them, making Active Storage a powerful solution for file management in Rails applications. 🚀


Where Are Files Stored in Rails 8 by Default?

By default, Rails 8 stores uploaded files using Active Storage’s disk service, meaning files are saved in the storage/ directory within your Rails project.

Default Storage Location:

  • Files are stored in:
    storage/
    ├── cache/ (temporary files)
    ├── store/ (permanent storage)
    └── variant/ (image transformations like resizing)
  • The exact file path inside storage/ is determined by the key column in the active_storage_blobs table. For example, if a blob entry has: key = 'xyz123abcd' then the file is stored at: storage/store/xyz123abcd

How to Change the Storage Location?

You can configure storage in config/storage.yml. For example:

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: us-east-1
  bucket: your_own_bucket-<%= Rails.env %>

Then, update config/environments/development.rb (or production.rb) to use:

config.active_storage.service = :local  # or :amazon for S3


How to Get the Stored File Path in Rails 8 Active Storage

Since Rails stores files in a structured directory inside storage/, the actual file path can be determined using the key stored in the active_storage_blobs table.

Get the File Path in Local Storage

If you’re using the Disk service (default for development and test), you can retrieve the stored file path manually:

blob = ActiveStorage::Blob.last
file_path = Rails.root.join("storage", "store", blob.key)
puts file_path

🔹 Example Output:

/your_project/storage/store/xyz123abcd

💡 This path is internal and cannot be accessed directly from a browser.


How to Get the File URL

Instead of accessing the internal path, Active Storage provides methods to generate URLs for public access.

1. Generate a URL for Direct Access

If you want a publicly accessible URL, you can use:

Rails.application.routes.url_helpers.rails_blob_url(blob, host: "http://localhost:3000")

🔹 Example Output:

http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiO...--filename.jpg

This redirects to the actual file storage location.

2. Get a Temporary Signed URL

For direct storage services like S3 or Google Cloud Storage, you can generate a signed URL:

blob.service_url

🔹 Example Output (for S3 storage):

https://your-s3-bucket.s3.amazonaws.com/xyz123abcd?X-Amz-Signature=...

🔹 Example Output (for local storage, using Disk service):

http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiO...

This signed URL expires after a set time (default is a few minutes).

3. Get a Variant URL for an Image

If your file is an image and you want a resized version, use:

variant = blob.variant(resize: "300x300").processed
Rails.application.routes.url_helpers.rails_representation_url(variant, host: "http://localhost:3000")

🔹 Example Output:

http://localhost:3000/rails/active_storage/representations/abcdxyz.../resize_300x300.jpg

Summary

TaskCommand
Get internal file pathRails.root.join("storage", "store", blob.key)
Get public file URLRails.application.routes.url_helpers.rails_blob_url(blob, host: "http://localhost:3000")
Get signed (temporary) URL (If your model has has_one/many_attached)blob.service_url
Get resized image URLRails.application.routes.url_helpers.rails_representation_url(blob.variant(resize: "300x300").processed, host: "http://localhost:3000")
  • Files are stored in the storage/ directory by default.
  • Use rails_blob_url or service_url to get an accessible URL.
  • Use variant to generate resized versions.
  • For production, it’s best to use a cloud storage service like Amazon S3.

Understanding has_one_attached and has_many_attached in Rails 8

Rails 8 provides a built-in way to handle file attachments through Active Storage. The key methods for attaching files to models are:

  1. has_one_attached – For a single file attachment.
  2. has_many_attached – For multiple file attachments.

Let’s break down what they do and why they are useful.

1. has_one_attached

This is used when a model should have a single file attachment. For example, a User model may have only one profile picture.

Usage:

class User < ApplicationRecord
  has_one_attached :avatar
end

How It Works:

  • When you upload a file, Active Storage creates an entry in the active_storage_blobs table.
  • The active_storage_attachments table links this file to the record.
  • If a new file is attached, the old one is automatically replaced.

Example: Attaching and Displaying an Image

user = User.find(1)
user.avatar.attach(io: File.open("/path/to/avatar.jpg"), filename: "avatar.jpg", content_type: "image/jpeg")

# Checking if an avatar exists
user.avatar.attached? # => true

# Displaying the image in a view
<%= image_tag user.avatar.variant(resize: "100x100").processed if user.avatar.attached? %>

2. has_many_attached

Use this when a model can have multiple file attachments. For instance, a Product model may have multiple images.

Usage:

class Product < ApplicationRecord
  has_many_attached :images
end

How It Works:

  • Multiple files can be attached to a single record.
  • Active Storage tracks all file uploads in the active_storage_blobs and active_storage_attachments tables.
  • Deleting an attachment removes it from storage.

Example: Attaching and Displaying Multiple Images

product = Product.find(1)
product.images.attach([
  { io: File.open("/path/to/image1.jpg"), filename: "image1.jpg", content_type: "image/jpeg" },
  { io: File.open("/path/to/image2.jpg"), filename: "image2.jpg", content_type: "image/jpeg" }
])

# Checking if images exist
product.images.attached? # => true

# Displaying all images in a view
<% if product.images.attached? %>
  <% product.images.each do |image| %>
    <%= image_tag image.variant(resize: "200x200").processed %>
  <% end %>
<% end %>

Benefits of Using has_one_attached & has_many_attached

  1. Simplifies File Attachments – Directly associates files with Active Record models.
  2. No Need for Extra Tables – Unlike some gems (e.g., CarrierWave), Active Storage doesn’t require additional tables for storing file paths.
  3. Easy Cloud Storage Integration – Works seamlessly with Amazon S3, Google Cloud Storage, and Azure.
  4. Variant Processing – Generates resized versions of images using variant (e.g., thumbnails).
  5. Automatic Cleanup – Old attachments are automatically removed when replaced.

Final Thoughts

Active Storage in Rails 8 provides a seamless way to manage file uploads, integrating directly with models while handling storage efficiently. By understanding how it processes uploads internally, we can better optimize performance and ensure a smooth user experience.

In an upcoming blog, we’ll dive deeper into Turbo Streams and how they enhance real-time updates in Rails applications.

Setting Up ⚙️ SSH in your system

SSH (Secure Shell) is used to establish secure remote connections over an unsecured network, enabling secure access, management, and data transfer on remote systems, including running commands, transferring files, and managing applications.

Setup SSH keys:

To create an SSH key and add it to your GitHub account, follow these steps:

1. Generate an SSH Key

ssh-keygen -t ed25519 -C "your-email@example.com"
  • Replace "your-email@example.com" with your GitHub email.
  • If prompted, press Enter to save the key in the default location (~/.ssh/id_ed25519).
  • Set a passphrase (optional for security).

2. Start the SSH Agent

eval "$(ssh-agent -s)"

3. Add the SSH Key to the Agent

ssh-add ~/.ssh/id_ed25519

4. Copy the SSH Key to Clipboard

cat ~/.ssh/id_ed25519.pub | pbcopy   # macOS
cat ~/.ssh/id_ed25519.pub | xclip -selection clipboard   # Linux
clip < ~/.ssh/id_ed25519.pub   # Windows (Git Bash)

(If xclip is not installed, use sudo apt install xclip on Linux)


5. Add the SSH Key to GitHub

  • Go to GitHub → Settings → SSH and GPG keys (GitHub SSH Keys).
  • Click New SSH Key.
  • Paste the copied key into the field and give it a title.
  • Click Add SSH Key.

6. Test the SSH Connection

ssh -T git@github.com

You should see a message like:

Hi username! You've successfully authenticated, but GitHub does not provide shell access.

Now you can clone, push, and pull repositories without entering your GitHub password!

You may be wondering what is ed25519 ?

ed25519 is a modern cryptographic algorithm used for generating SSH keys. It is an alternative to the older RSA algorithm and is considered more secure and faster.

Why Use ed25519 Instead of RSA?

  1. Stronger Securityed25519 provides 128-bit security, while RSA requires a 4096-bit key for similar security.
  2. Smaller Key Size – The generated keys are much shorter than RSA keys, making them faster to use.
  3. Faster Performanceed25519 is optimized for speed, especially on modern hardware.
  4. Resistant to Certain Attacks – Unlike RSA, ed25519 is resistant to side-channel attacks.

Why GitHub Recommends ed25519?

  • Since 2021, GitHub suggests using ed25519 over RSA because of better security and efficiency.
  • Older RSA keys (e.g., 1024-bit) are now considered weak.

When Should You Use ed25519?

  • Always, unless you’re working with old systems that do not support it.
  • If you need maximum security, speed, and smaller key sizes.

Example: Creating an ed25519 SSH Key

ssh-keygen -t ed25519 -C "your-email@example.com"

This creates a strong and secure SSH key for GitHub authentication.

What is the SSH Agent?

The SSH agent is a background process that securely stores your SSH private keys and manages authentication.

Instead of entering your private key passphrase every time you use SSH (e.g., for git push), the agent remembers your key after you add it.


Why Do We Need the SSH Agent?

  1. Avoid Entering Your Passphrase Repeatedly
  • If your SSH key has a passphrase, you would normally need to enter it every time you use git push or ssh.
  • The agent caches the key in memory so you don’t need to enter the passphrase every time.
  1. Automatic Authentication
  • Once the agent has your key, it can sign SSH requests for authentication automatically.
  1. Keeps Your Private Key Secure
  • Your private key stays in memory and is not exposed on disk after adding it.

What Do These Commands Do?

1️⃣ Start the SSH Agent

eval "$(ssh-agent -s)"
  • This starts the SSH agent in the background.
  • The eval "$(ssh-agent -s)" command initializes the agent and prints its process ID (PID), confirming it’s running.

2️⃣ Add the SSH Key to the Agent

ssh-add ~/.ssh/id_ed25519
  • This adds your private key (~/.ssh/id_ed25519) to the agent.
  • Now, the agent will remember the key and use it when making SSH connections.

How to Check If Your Key is Added?

Run:

ssh-add -l

This lists all keys currently added to the agent. If your key is listed, it means the agent is working correctly.


Example: How This Helps?

Without the agent:

git push origin main
# Every time, you need to enter your SSH passphrase

With the agent:

ssh-add ~/.ssh/id_ed25519
git push origin main
# No need to enter your passphrase again!

Conclusion
  • The SSH agent stores your private key securely in memory.
  • It automates authentication, so you don’t need to enter your passphrase for every SSH request.
  • Adding the key with ssh-add lets Git and SSH use it without prompting.

Now, you can git push and git pull seamlessly!

But the question is: Can this make unauthorized persons to access with this commands without ssh passphrase?


Does using ssh-agent make your system less secure? Let’s break it down.

Can Someone Else Use My SSH Key Without a Passphrase?

✅ No, If Your System is Secure

  • The SSH agent only runs under your user session.
  • Other users on the same machine cannot access your agent.
  • Your private key is never written to disk in an unprotected way.
  • The agent only allows authentication, not direct access to your key.

⚠️ Yes, If Someone Gets Access to Your Session

  • If an attacker gains access to your system (e.g., via malware, or if you leave your computer unlocked), they might use your active SSH agent to authenticate without needing your passphrase.
  • However, they cannot extract your private key from the agent.

How to Improve Security?

If you want extra security, here are a few things you can do:

1️⃣ Remove Keys from Agent When Not Needed

After using your SSH key, you can remove it from the agent:

ssh-add -D

This removes all stored keys. Next time you push, you’ll need to re-enter your passphrase.


2️⃣ Use -t (Timeout) for Auto Removal

To automatically remove the key after a set time:

ssh-add -t 3600 ~/.ssh/id_ed25519  # Removes the key after 1 hour


3️⃣ Lock Your Screen When Away

If someone gets access to your logged-in session, they could use your agent to authenticate without needing the passphrase.

Always lock your screen (Ctrl + L or Win + L on Windows/Linux, Cmd + Ctrl + Q on Mac) when stepping away.


4️⃣ Disable Agent Forwarding (Extra Security)

By default, SSH agent forwarding (ssh -A) can expose your keys to remote servers. If you don’t need it, disable it by editing:

nano ~/.ssh/config

And adding:

Host *
    ForwardAgent no

Summary
  1. The SSH agent only runs in your session, so no one else can access it unless they get control of your user session.
  2. Attackers cannot steal your private key from the agent, but if they have access to your session, they could use it.
  3. To be safe, remove keys when not needed (ssh-add -D), use timeouts (-t), and always lock your computer.

You’re now both secure and productive with SSH! 🚀

Git Workflow 🔀: Best Practices for Your Development Process

Scenario:

  • You have a main branch (main).
  • You create feature branches (e.g., feature-x) for development.
  • You want to keep your history clean and structured.

1️⃣ git merge → Use When You Want to Keep Full History (Default Approach)

📌 What It Does?

  • Combines feature branch commits into main, keeping all commit history.
  • Creates a new merge commit (Fast-forward if no diverging commits).
  • Easy to understand but can clutter history with many small commits.

📌 When to Use?

✅ If you want a full commit history, including every small commit.
✅ When working in teams where visibility of each commit matters.

📌 Example (Merging feature-x into main):

git checkout main
git pull origin main   # Ensure main is up to date
git merge feature-x
git push origin main

2️⃣ git rebase → Use When You Want a Clean, Linear History

📌 What It Does?

  • Moves (reapplies) all commits from the feature branch on top of the latest main.
  • No merge commit → keeps history linear and clean.
  • Can cause conflicts if multiple people are rebasing.

📌 When to Use?

✅ When working solo and want a clean, linear history.
✅ Before merging a feature branch to avoid unnecessary merge commits.
✅ If you regularly update your feature branch with main.

📌 Example (Rebasing feature-x onto main Before Merging):

git checkout feature-x
git pull origin main   # Ensure main is up to date
git rebase main        # Moves feature branch commits on top of the latest main

If there are conflicts, resolve them, then:

git rebase --continue

Then merge into main:

git checkout main
git merge feature-x   # Fast-forward merge (clean)
git push origin main

3️⃣ git squash → Use When You Want a Single Commit for a Feature

📌 What It Does?

  • Combines multiple commits in the feature branch into one commit before merging.
  • Keeps history very clean but loses commit granularity.

📌 When to Use?

✅ If a feature branch has many small commits (e.g., fix typo, refactor, debugging).
✅ When you want one clean commit per feature.
✅ If your team follows a “one commit per feature” policy.

📌 Example (Squashing Commits Before Merging into main):

Step 1: Squash commits interactively

git checkout feature-x
git rebase -i main

Step 2: Mark commits to squash

You’ll see:

pick abc123 First commit
pick def456 Second commit
pick ghi789 Third commit

Change all but the first pick to squash (s), like this:

pick abc123 First commit
squash def456 Second commit
squash ghi789 Third commit

Save and exit.

Step 3: Push the squashed commit

git push origin feature-x --force  # Required after rewriting history

Step 4: Merge into main

git checkout main
git merge feature-x
git push origin main

Another git squash example. Suppose you have created a branch for adding a feature and you do 2 more commits one for refactoring and other for commenting on the feature. Now you have 3 commits in the feature-x branch.

Now you think better I would commit these 3 commit together and push it as one feature commit. Now what you do? follow the bel0w steps:

# this tells git you want to do some action upon the last 3 commits
git rebase -i HEAD~3

Now you can see 3 commits in your default editor prefixed: pick. Change the last 2 commit pick to squash and save the file. Now Another file popup in the editor to change the commit message for this one squashed commit. Ignore the messages start with # . Keep/modify the commit message which is not starting with # and save the file. This rewrites the history and save as one commit.

Now check the commit history:

git log --oneline

Checkout git HEAD in our post: https://railsdrop.com/2025/04/06/git-best-practices-git-head-useful-commands-commit-message/

Which One to Use in Your Development Process?

ScenarioUse merge?Use rebase?Use squash?
Merging a feature branch✅ Yes (keeps history)✅ Yes (cleaner history)✅ Yes (one commit per feature)
Keeping history linear❌ No✅ Yes✅ Yes
Keeping every commit✅ Yes✅ Yes❌ No (loses small commits)
Team collaboration✅ Yes (safer)⚠️ Be careful (can rewrite history)✅ Yes (if team agrees)
Avoiding merge commits❌ No✅ Yes✅ Yes

Recommendation for Your Workflow 🔀

For daily work, keep your feature branch updated with main:

git checkout feature-x 
git pull origin main 
git rebase main

Before merging into main, If you want full history:

git merge feature-x

If you want a clean history:

git rebase main

If you want a single commit per feature:

git squash

Merge into main and push:

git checkout main 
git merge feature-x 
git push origin main

🤔 What’s the Problem with Merge Commits?

A merge commit happens when you run:

git merge feature-x

and Git creates an extra commit to record the merge.

Example merge commit:

commit abc123 (HEAD -> main)
Merge: def456 ghi789
Author: You <you@example.com>
Date:   Sat Mar 29 12:00:00 2025 +0000

    Merge branch 'feature-x' into 'main'

Why Do Some Developers Avoid Merge Commits?

1️⃣ Cluttered History

  • Merge commits pollute history when feature branches have multiple commits.
  • Running git log shows lots of merge commits, making it harder to track actual code changes.
  • Example of messy history with merge commits:
    * Merge branch 'feature-x' (Merge Commit)
    * Fix typo in error message
    * Add validation for user input
    * Implement user authentication
    * Merge branch 'feature-y' (Merge Commit)
    * Fix UI issue

2️⃣ Harder to Track Changes

  • git log gets filled with merge commits instead of meaningful commits.
  • git blame might point to a merge commit, making debugging harder.

3️⃣ Confusing Graph with Many Branches

  • Running git log --graph --oneline --all shows a complex branching history:
    * abc123 Merge branch ‘feature-x’
    |\
    | * ghi789 Fix authentication bug
    | * def456 Add new login method
    | 123456 Merge branch ‘feature-y’
    |/
  • If you rebase instead, the history stays linear.

🛠️ When Do Devs Avoid Merge Commits?

1️⃣ When They Want a Clean, Linear History

✅ Use git rebase instead of git merge to avoid merge commits.
Example:

git checkout feature-x
git rebase main
git checkout main
git merge feature-x  # No merge commit (fast-forward merge)

🔹 This way, the commits from feature-x appear directly on top of main.

2️⃣ When They Squash Multiple Commits into One

✅ Use git rebase -i main to combine multiple commits into one before merging.
Example:

git checkout feature-x
git rebase -i main  # Squash commits
git checkout main
git merge feature-x  # No unnecessary commits

🔹 This results in one clean commit per feature.

3️⃣ When Working on a Team with Many Developers

  • If multiple people merge their branches without rebasing, Git history gets full of merge commits.
  • Teams using trunk-based development or feature branch workflows often rebase instead of merging.

✅ Solution → Always rebase before merging:

git checkout feature-x
git pull --rebase origin main
git push origin feature-x

When Are Merge Commits Actually Useful?

Merge commits are not always bad! Sometimes, they are necessary:

  1. Merging Long-Lived Feature Branches
    • If a branch exists for weeks or months, it has many commits.
    • A merge commit documents when the feature was merged.
  2. Merging Changes from a Release Branch
    • Example: Merging release-v1.0 into main should have a merge commit.
  3. Handling Complex Conflicts
    • When resolving big merge conflicts, a merge commit shows exactly when conflicts were fixed.

🔑 Final Answer: When to Avoid Merge Commits?

🚫 Avoid merge commits when you want a clean history
✅ Use rebasing (git rebase) before merging to keep history linear.
✅ Use squashing (git rebase -i) to avoid unnecessary commits in a feature branch.

🔹 If you’re working solo or in a small team, rebase before merging to keep history simple.
🔹 If you’re working in a large team with long-lived branches, merge commits may be useful for tracking.


🔥 Finally Best Practices for Your Development Process

Before merging, update your branch without a merge commit:

git checkout feature-x
git pull --rebase origin main

Squash unnecessary commits (optional):

git rebase -i main

Merge the branch (fast-forward, no merge commit):

git checkout main
git merge feature-x  # Fast-forward merge, no merge commit

This keeps your history clean and easy to read!

Setup 🛠 Rails 8 App – Part 4: Tailwind CSS 🎨 into the action

Does Tailwind Take Time?

  • If you haven’t used Tailwind before, expect a slight learning curve (1-2 days) to get comfortable.
  • But once you grasp utility classes, styling becomes faster than Bootstrap.

Fastest Way to Set Up Tailwind in Rails 8

1. Install Tailwind

   rails new myapp --css=tailwind

Or, if you already have a Rails app:

   ✗ bundle add tailwindcss-rails
     Fetching gem metadata from https://rubygems.org/.........
     Resolving dependencies...
     Fetching gem metadata from https://rubygems.org/.........
     Resolving dependencies...
     Fetching tailwindcss-ruby 4.0.17 (arm64-darwin)
     Installing tailwindcss-ruby 4.0.17 (arm64-darwin)
     Fetching tailwindcss-rails 4.2.1
     Installing tailwindcss-rails 4.2.1
   
  ✗ rails tailwindcss:install
      apply  /Users/abhilash/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/tailwindcss-rails-4.2.1/lib/install/install_tailwindcss.rb
  Add Tailwindcss container element in application layout
      insert    app/views/layouts/application.html.erb
      insert    app/views/layouts/application.html.erb
  Build into app/assets/builds
      create    app/assets/builds
      create    app/assets/builds/.keep
      append    .gitignore
  Add default /Users/xxxxx/rails/design_studio/app/assets/tailwind/application.css
      create    app/assets/tailwind/application.css
  Add default Procfile.dev
      create    Procfile.dev
  Ensure foreman is installed
         run    gem install foreman from "."
         Fetching foreman-0.88.1.gem
         Successfully installed foreman-0.88.1

Refer: https://tailwindcss.com/docs/installation/framework-guides/ruby-on-rails

Note: You can see this create a Procfile.dev file and installs foreman gem.

The foreman gem in Rails is used to manage and run multiple processes in development using a Procfile. It is particularly useful when your Rails application depends on several background services that need to run simultaneously.

Why is foreman used?

  • It allows you to define and manage multiple services (like Rails server, Tailwind compiler, Sidekiq, etc.) in a single command.
  • Ensures that all necessary processes start together, making development easier.
  • Helps simulate production environments where multiple services need to run concurrently.

Who is using foreman? Rails or Tailwind CSS?

  • The foreman gem itself is not specific to Rails or Tailwind CSS—it is a general-purpose process manager.
  • In our case, both Rails and Tailwind CSS are using foreman.
    • Rails: You can use foreman to start Rails server, background jobs, and Webpack.
    • Tailwind CSS: Since Tailwind needs a process to watch and compile CSS files (using npx tailwindcss -i input.css -o output.css --watch), foreman helps keep this process running.

What is Procfile.dev in Tailwind CSS?

  • When you install Tailwind in Rails, a Procfile.dev is created to define the processes required for development.
  • Example Procfile.dev for Tailwind and Rails:
    web: bin/rails server -p 3000
    js: yarn build --watch
    css: bin/rails tailwindcss:watch
    • web: Starts the Rails server.
    • js: Watches and compiles JavaScript files (if using esbuild or webpack).
    • css: Watches and compiles Tailwind CSS files.

How to use foreman?

  • Run the following command to start all processes defined in Procfile.dev: bin/dev
    • This starts the Rails server, the Tailwind CSS watcher, and other necessary processes.

The foreman gem is used as a process manager to run multiple services in development. In our case, both Rails and Tailwind CSS are using it. It ensures that Tailwind’s CSS compilation process runs alongside the Rails server.

2. Use Tailwind Classes in Views

Example:

   <div class="container mx-auto p-4">
     <h1 class="text-blue-500 text-3xl font-bold">Welcome to My App</h1>
     <button class="bg-green-500 text-white px-4 py-2 rounded">Click Me</button>
   </div>

This keeps your CSS minimal, avoids custom stylesheets, and helps you learn Tailwind naturally while building your app.

Here’s a Tailwind CSS Cheat Sheet to help you get started quickly with your Rails 8 app.

📌 1. Layout & Spacing

<div class="container mx-auto p-4">
  <div class="m-4 p-4 bg-gray-200">Margin & Padding</div>
</div>
  • mx-auto → Center content horizontally
  • p-4 → Padding (4 * 4px = 16px)
  • m-4 → Margin (4 * 4px = 16px)

🎨 2. Colors

<div class="bg-blue-500 text-white p-4">Blue Background</div>
<div class="text-red-600">Red Text</div>
  • Colors: gray, red, blue, green, etc.
  • Shades: 100 - 900 (lighter → darker)

Refer: https://tailwindcss.com/docs/background-color


🔤 3. Typography

<p class="text-lg font-bold">Large Bold Text</p>
<p class="text-sm italic">Small Italic Text</p>
  • text-lg → Large text
  • text-sm → Small text
  • font-bold → Bold text
  • italic → Italic text

📏 4. Width & Height

<div class="w-1/2 h-32 bg-green-300">50% Width, 128px Height</div>
  • w-1/2 → 50% width
  • w-full → Full width
  • h-32 → 128px height

🔳 5. Flexbox & Grid

<div class="flex justify-center items-center h-screen">
  <div class="bg-blue-500 p-4 text-white">Centered Box</div>
</div>
  • flex → Enables flexbox
  • justify-center → Centers horizontally
  • items-center → Centers vertically

🔘 6. Buttons

<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Click Me
</button>
  • hover:bg-blue-700 → Darker blue on hover
  • rounded → Rounded corners

🔗 7. Borders & Shadows

<div class="border border-gray-400 shadow-lg p-4">Border & Shadow</div>
  • border → Default border
  • border-gray-400 → Gray border
  • shadow-lg → Large shadow

📲 8. Responsive Design

<div class="p-4 text-sm md:text-lg lg:text-xl">
  Text size changes on different screens!
</div>
  • sm: → Small screens (≥ 640px)
  • md: → Medium screens (≥ 768px)
  • lg: → Large screens (≥ 1024px)

✨ 9. Animations & Transitions

<button class="bg-green-500 transition duration-300 hover:bg-green-700">
  Smooth Transition
</button>
  • transition → Enables animations
  • duration-300 → 300ms transition speed
  • hover:bg-green-700 → Change color on hover

🚀 Quick Setup for Your Rails 8 App

  1. Install Tailwind
   rails new myapp --css=tailwind
  1. Use Tailwind Classes in Views
   <div class="text-center p-6 text-2xl text-blue-500">Hello Tailwind!</div>

🔥 Bonus: Try Tailwind Play for live previews!

Tailwind in design studio

Check our Rails 8 app for tailwind implementation: https://github.com/abhilashak/design_studio/blob/main/bin/dev

I have added Tailwind CSS to our design_studio app. You can check the commit here: https://github.com/abhilashak/design_studio/commit/07db459

Track our Github Issue for tailwind task: https://github.com/abhilashak/design_studio/issues/1

The above issue will be automatically closed if you follow our Git commit message principle, that is mentioned in the below post:

Finally add the Tailwind Plugin Tailwind CSS IntelliSense for VS Code to make development easy.

to be continued.. 🚀

Setup 🛠 Rails 8 App – Part 3: Git setup, modify gitignore, git config

So now let’s push the code to github repository. Before that there is some final checks need to be done from our end.

Check .gitignore file and update

✅ Files/Folders to Include in .gitignore

Here’s a breakdown of which files/folders should be added to .gitignore in your Rails project:

These files are user-specific or environment-specific and should not be committed to Git.

1️⃣ .dockerignore → ❌ Ignore from .gitignore

  • Keep this file if you’re using Docker.
  • It’s like .gitignore but for Docker, helping to reduce Docker image size.
  • Do not add it to .gitignore if you need it.

2️⃣ .github/ → ✅ Add to .gitignore (If personal CI/CD configs)

  • If this contains GitHub Actions or issue templates, keep it in the repo.
  • If it’s just for personal workflows, add it to .gitignore.

3️⃣ .kamal/ → ✅ Add to .gitignore

  • This contains deployment secrets and configuration files for Kamal (deployment tool).
  • It’s usually auto-generated and should not be committed.

4️⃣ .vscode/ → ✅ Add to .gitignore

  • User-specific VSCode settings, should not be committed.
  • Different developers use different editors.

Keep These Files in Git (Don’t Add to .gitignore)

These files are important for project configuration.

1️⃣ .gitattributes → ❌ Keep in Git

  • Defines how Git handles line endings and binary files.
  • Helps avoid conflicts on Windows/Linux/Mac.

2️⃣ .gitignore → ❌ Keep in Git

  • Defines ignored files, obviously should not be ignored itself.

3️⃣ .rubocop.yml → ❌ Keep in Git

  • This is for Rubocop linting rules, which helps maintain coding style.
  • All developers should follow the same rules.

4️⃣ .ruby-version → ❌ Keep in Git

  • Defines the Ruby version for the project.
  • Ensures all team members use the same Ruby version.

Final .gitignore Entries Based on Your Files

# Ignore log files, temp files, and dependencies
/log/
/tmp/
.bundle/
/node_modules/

# Manually added
# Ignore editor & environment-specific configs
.vscode/

# Ignore deployment configs
.kamal/

# Ignore personal GitHub configs (if applicable)
.github/

Final Summary

FolderInclude in Git?Why?
log/❌ IgnoreDynamically generated logs
public/✅ Keep (except public/assets/)Static files like favicon, error pages
script/✅ KeepOld Rails script files (if used)
storage/❌ IgnoreActiveStorage uploads (except seed/)
test/✅ KeepContains important test cases
tmp/❌ IgnoreTemporary runtime files
vendor/❌ Ignore (except custom libraries)Third-party libraries

First time git setup

# You can view all of your settings and where they are coming from using

git config --list --show-origin

# Your Identity: The first thing you should do is to set your user name and email address.

git config --global user.name "Abhilash"
git config --global user.email abhilash@example.com

# configure the default text editor that will be used when Git needs you to type in a message

git config --global core.editor emacs
git config --global core.editor "code --wait"     # vs code
git config --global -e      # verify editor

# command to list all the settings Git can find at that point

git config --list
git config user.name

Add your ssh key to your github. Check the post: https://railsdrop.com/2025/03/30/setting-up-ssh-in-your-system/

Initial commit: Execute the Git commands

Run the following commands in your rails app folder:

git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:abhilashak/design_studio.git
git remote -v     # check the remote endpoints
git push -u origin main

git log      # check commit details

What It Does:

git branch -M main
  1. Renames the current branch to main.
  2. The -M flag forcefully renames the branch (overwrites if needed).

Common Use Case:

  • If your branch is named master and you want to rename it to main (which is now the default in many repositories).
  • If you created a branch with a different name and want to standardise it as main.

Example Usage:

git branch -M main
git push -u origin main

This renames the current branch to main and then pushes it to the remote repository.

Use github ssh option: it checks the ssh key that you setup in your github account for that machine.
http option asks your github credentials to login.

The -u option in the command:

git push -u origin main

What Does -u Do?

The -u flag stands for --set-upstream. It sets the upstream branch for main, which means:

  • It links your local branch (main) to the remote branch (main on origin).
  • After running this command once, you can simply use:
  git push

instead of git push origin main, because Git now knows where to push.

Example Use Case:

If you just created a new branch (main in this case) and are pushing it for the first time:

git push -u origin main

This ensures that main always pushes to origin/main without needing to specify it every time.

After Running This Command:

✅ Next time, you can simply use:

git push
git pull

without needing to specify origin main again.

For better git work flow

Check the post: https://railsdrop.com/2025/03/29/git-workflow-best-practices-for-your-development-process/

Want to See Your Upstream Branch?

Run:

git branch -vv

This shows which remote branch your local branches are tracking.

to be continued.. 🚀


Setup 🛠 Rails 8 App – Part 2: Command line, VS Code, RuboCop, Server, Action text, Image processing

In the first part of this guide, we covered setting up a Rails 8 app with essential configurations. In this follow-up, we’ll go over optimizing command-line usage, setting up VS Code for development, running migrations, styling the app, and enabling Action Text.


1. Optimizing Command-Line Usage with Aliases

One of the best ways to speed up development is to create shortcuts for frequently used commands. You can do this by adding aliases to your shell configuration.

Steps to Add an Alias:

  1. Open your shell configuration file:
    vim ~/.zshrc
  2. Search for the alias section:
    <esc> / alias <enter>
  3. Add your alias:
    alias gs="git status"
  4. Save and exit:
    <esc> :wq
  5. Reload your configuration:
    source ~/.zshrc
  6. Use your new alias:
    gs

This method saves time by allowing you to run frequently used commands more quickly.


2. Using Terminal Efficiently in VS Code

By default, VS Code uses `Ctrl + “ to toggle the terminal, which may not be intuitive. You can change this shortcut:

  1. Open VS Code.
  2. Go to Settings → Keyboard Shortcuts.
  3. Search for Toggle Terminal.
  4. Click Edit and change it to Ctrl + Opt + T for easier access.

3. Setting Up RuboCop in VS Code

RuboCop ensures your Ruby code follows best practices. Here’s how to set it up:

Checking RuboCop from the Terminal:

rubocop .

VS Code Setup:

  1. Open Command Palette (Cmd + Shift + P) and search for “Lint by RuboCop”.
  2. Go to Extensions Tab and install “VS Code RuboCop”.
  3. In VS Code Settings, search for “Rubocop” and check Ruby -> Rubocop -> Execute Path.
  4. Find the RuboCop installation path: whereis rubocop Example output: ~/.local/share/mise/installs/ruby/3.4.1/bin/rubocop/
  5. Update the Execute Path in VS Code to: ~/.local/share/mise/installs/ruby/3.4.1/bin/
  6. If RuboCop still returns an empty output, check .rubocop.yml in your project: ~/rails/design_studio/.rubocop.yml
  7. If the issue persists, ensure the gem is installed: gem install rubocop
  8. Restart VS Code from the Rails project root: code .

For more details, check the official documentation: RuboCop Usage


4. Running Migrations and Starting the Server

Running Migrations:

rails db:migrate -t

You can check the file: db/schema.rb You can see the active storage tables for attachments and other info.

Starting the Rails Server:

bin/dev

Access your application at: http://localhost:3000/

Check the product routes:


5. Adding Basic Styles

To quickly improve the appearance of your site, add this to application.html.erb:

<%= stylesheet_link_tag "https://cdn.simplecss.org/simple.css" %>

Alternatively, use Tailwind CSS for a more modern approach.

An example of convention over configurations in view file in Rails:

<div id="products">
  <%= render brand.products %>
</div>

Rails will look into the views/products/ folder and fetch right partial file that match.


6. Debugging with Rails Console

If an error occurs, you can inspect variables and data in the Rails console:

rails console


7. Installing Action Text for Rich Text Editing

Action Text is not installed by default but can be added easily:

rails action_text:install

This command:

  • Installs JavaScript dependencies
  • Creates necessary stylesheets
  • Generates database migrations for rich text

Running Migrations:

rails db:migrate -t

If you encounter an error about missing gems:

Could not find gem 'image_processing (~> 1.2)'

Run:

bundle install
rails db:migrate -t

Updating the Product Model:

Add this to app/models/product.rb:

has_rich_text :description

Updating the Form View:

In app/views/products/_form.html.erb, replace the description input with:

<%= form.rich_text_area :description %>

Now, visiting the new product page should display a rich text editor.


8. Solving Image Processing Errors

If you see an error like:

LoadError: Could not open library 'vips.42'

Install libvips to resolve it:

brew install vips


Conclusion

In this second part, we covered:

  • Optimizing terminal usage with aliases.
  • Configuring VS Code for efficient development.
  • Running migrations and starting the Rails server.
  • Enhancing the UI with styles.
  • Debugging using Rails console.
  • Installing and configuring Action Text for rich text support.

Stay tuned for more Rails 8 improvements!

Part 3: https://railsdrop.com/2025/03/25/setup-rails-8-app-git-setup-gitignore-part-3/

To be continued… 🚀

Setup 🛠 Rails 8 App – Part 1: Setup All Necessary Configurations | Ruby | Rails setup | Kamal | Rails Generations

Ruby on Rails 8 introduces several improvements that make development easier, more secure, and more maintainable. In this guide, we’ll walk through setting up a new Rails 8 application while noting the significant configurations and features that come out of the box.

1. Check Your Ruby and Rails Versions

If not installed Ruby 3.4 and Rails 8.0 please check: https://railsdrop.com/2025/02/11/installing-and-setup-ruby-3-rails-8-vscode-ide-on-macos-in-2025/

Before starting, ensure that you have the correct versions of Ruby and Rails installed:

$ ruby -v
ruby 3.4.1

$ rails -v
Rails 8.0.1

If you don’t have these versions installed, update them using your package manager or version manager (like rbenv or RVM).

2. Create a New Rails 8 Application

Run the following command to create a new Rails app:

$ rails new design_studio

Noteworthy Files and Directories Created

Here are some interesting files and directories that are generated with a new Rails 8 app:

 create  .ruby-version
 create  bin/brakeman
 create  bin/rubocop
 create  bin/docker-entrypoint
 create  .rubocop.yml
 create  .github/workflows
 create  .github/workflows/ci.yml
 create  config/cable.yml
 create  config/storage.yml
 create  config/initializers/content_security_policy.rb
 create  config/initializers/filter_parameter_logging.rb
 create  config/initializers/new_framework_defaults_8_0.rb

Key Takeaways:

  • Security & Code Quality Tools: Brakeman (security scanner) and RuboCop (code style linter) are included by default.
  • Docker Support: The presence of bin/docker-entrypoint suggests better built-in support for containerized deployment.
  • GitHub Actions Workflow: The .github/workflows/ci.yml file provides default CI configurations.
  • Enhanced Security: The content_security_policy.rb initializer helps enforce a strict security policy.
  • New Rails Defaults: The new_framework_defaults_8_0.rb initializer helps manage breaking changes in Rails 8.

Rails automatically creates the following during the creation of the rails new app.

a. Configuring Import Maps and Installing Turbo & Stimulus

Rails 8 still defaults to Import Maps for JavaScript package management, avoiding the need for Node.js and Webpack:

$ rails turbo:install stimulus:install

This creates the following files:

create    config/importmap.rb
create    app/javascript/controllers
create    app/javascript/controllers/index.js
create    app/javascript/controllers/hello_controller.js
append    config/importmap.rb

Key Takeaways:

  • Import Maps: Defined in config/importmap.rb, allowing dependency management without npm.
  • Hotwired Support: Turbo and Stimulus are automatically configured for modern front-end development.
  • Generated Controllers: Stimulus controllers are pre-configured inside app/javascript/controllers/.

b. Deploying with Kamal

Kamal simplifies deployment with Docker and Kubernetes. Rails 8 includes built-in support:

$ bundle binstubs kamal
$ bundle exec kamal init

This results in:

Created .kamal/secrets file
Created sample hooks in .kamal/hooks

Key Takeaways:

  • Automated Deployment Setup: Kamal provides easy-to-use deployment scripts.
  • Secret Management: The .kamal/secrets file ensures secure handling of credentials.
  • Deployment Hooks: Custom hooks allow pre- and post-deployment scripts for automation.

c. Setting Up Caching and Queues with Solid Cache, Queue, and Cable

NOTE: Rails automatically creates this for you while creating the rails app.

Rails 8 includes Solid Cache, Solid Queue, and Solid Cable for enhanced performance and scalability:

$ rails solid_cache:install solid_queue:install solid_cable:install

This creates:

create  config/cache.yml
create  db/cache_schema.rb
create  config/queue.yml

Key Takeaways:

  • Caching Support: config/cache.yml manages application-wide caching.
  • Database-Powered Queue System: Solid Queue simplifies background job management without requiring external dependencies like Sidekiq.
  • Real-Time WebSockets: Solid Cable offers Action Cable improvements for real-time features.

3. Rails 8 Migration Enhancements

Rails 8 provides new shortcuts and syntax improvements for database migrations:

NOT NULL Constraints with ! Shortcut

You can impose NOT NULL constraints directly from the command line using !:

# Example for not null constraints: 
➜ rails generate model User name:string!

Type Modifiers in Migrations

Rails 8 allows passing commonly used type modifiers directly via the command line. These modifiers are enclosed in curly braces {} after the field type.

# Example for model generation: 
➜ rails generate model Product name:string description:text
# Example for passing modifiers: 
➜ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

Generating a Scaffold for the Product Model

Let’s generate a complete scaffold for our Product model:

✗ rails generate scaffold product title:string! description:text category:string color:string 'size:string{10}' 'mrp:decimal{7,2}' 'discount:decimal{7,2}' 'rating:decimal{1,1}'
➜  design_studio git:(main) ✗ rails -v
Rails 8.0.1
➜  design_studio git:(main) ✗ ruby -v
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24]
➜  design_studio git:(main) ✗ rails generate scaffold product title:string! description:text category:string color:string 'size:string{10}' 'mrp:decimal{7,2}' 'discount:decimal{7,2}' 'rating:decimal{1,1}'

Using the Rails Resource Generator

The rails g resource command is a powerful way to generate models, controllers, migrations, and routes all in one go. This is particularly useful when setting up RESTful resources in a Rails application.

Basic Syntax

➜ rails g resource product

This command creates the necessary files for a new resource, including:

  • A model (app/models/product.rb)
  • A migration file (db/migrate/)
  • A controller (app/controllers/product_controller.rb)
  • Routes in config/routes.rb
  • A test file (test/controllers/product_controller_test.rb or spec/)

Example Usage

To generate a Post resource with attributes:

➜ rails g resource Product title:string! description:text brand:references

This will:

  1. Create a Product model with title and description attributes.
  2. Add a brand_id foreign key as a reference.
  3. Apply a NOT NULL constraint on title (! shortcut).
  4. Generate a corresponding migration file.
  5. Set up routes automatically (resources :products).

Running the Migration

After generating a resource, apply the migration to update the database:

➜ rails db:migrate


Difference Between resource and scaffold

Rails provides both rails g resource and rails g scaffold, but they serve different purposes:

Featurerails g resourcerails g scaffold
Generates a Model
Generates a Migration
Generates a Controller✅ (empty actions)✅ (full CRUD actions)
Generates Views (HTML/ERB)✅ (index, show, new, edit, etc.)
Generates Routes
Generates Helper Files
Generates Tests
  • rails g resource is minimal—it generates only essential files without view templates. It’s useful when you want more control over how your views and controller actions are built.
  • rails g scaffold is more opinionated and generates full CRUD functionality with prebuilt forms and views, making it ideal for rapid prototyping.

If you need full CRUD functionality quickly, use scaffold. If you prefer a leaner setup with manual control over implementation, use resource.

Conclusion

Rails 8 significantly enhances the development experience with built-in security tools, CI/CD workflows, streamlined deployment via Kamal, and modern front-end support with Turbo & Stimulus. It also improves caching, background jobs, and real-time features with Solid tools.

These improvements make Rails 8 a robust framework for modern web applications, reducing the need for additional dependencies while keeping development efficient and secure.

Enjoy Rails! 🚀

Writing Perfect Active Record 🗒️ Queries in Ruby on Rails 8

Active Record (AR) is the heart of Ruby on Rails when it comes to database interactions. Writing efficient and readable queries is crucial for application performance and maintainability. This guide will help you master Active Record queries with real-world examples and best practices.


Setting Up a Sample Database

To demonstrate complex Active Record queries, let’s create a Rails app with a sample database structure containing multiple tables.

Generate Models & Migrations

rails new MyApp --database=postgresql
cd MyApp
rails g model User name:string email:string
rails g model Post title:string body:text user:references
rails g model Comment body:text user:references post:references
rails g model Category name:string
rails g model PostCategory post:references category:references
rails g model Like user:references comment:references
rails db:migrate

Database Schema Overview

  • users: Stores user information.
  • posts: Stores blog posts written by users.
  • comments: Stores comments on posts, linked to users and posts.
  • categories: Stores post categories.
  • post_categories: Join table for posts and categories.
  • likes: Stores likes on comments by users.

Basic Active Record Queries

1. Fetching All Records

User.all  # Returns all users (Avoid using it directly on large datasets as it loads everything into memory)

⚠️ User.all can lead to performance issues if the table contains a large number of records. Instead, prefer pagination (User.limit(100).offset(0)) or batch processing (User.find_each).

2. Finding a Specific Record

User.find(1)  # Finds a user by ID
User.find_by(email: 'john@example.com')  # Finds by attribute

3. Filtering with where vs having

Post.where(user_id: 2)  # Fetch all posts by user with ID 2

Difference between where and having:

  • where is used for filtering records before grouping.
  • having is used for filtering after group operations.

Example:

Post.group(:user_id).having('COUNT(id) > ?', 5)  # Users with more than 5 posts

4. Ordering Results

User.order(:name)  # Order users alphabetically
Post.order(created_at: :desc)  # Order posts by newest first

5. Limiting Results

Post.limit(5)  # Get the first 5 posts

6. Selecting Specific Columns

User.select(:id, :name)  # Only fetch ID and name

7. Fetching Users with a Specific Email Domain

User.where("email LIKE ?", "%@gmail.com")

8. Fetching the Most Recent Posts

Post.order(created_at: :desc).limit(5)

9. Using pluck for Efficient Data Retrieval

User.pluck(:email)  # Fetch only emails as an array

10. Checking if a Record Exists Efficiently

User.exists?(email: 'john@example.com')

11. Including Associations (eager loading to avoid N+1 queries)

Post.includes(:comments).where(comments: { body: 'Great post!' })


Advanced Queries with Joins

1. Joining Tables (INNER JOIN)

Post.joins(:user).where(users: { name: 'John' })

2. Self Join Example

A self-join is useful when dealing with hierarchical relationships, such as an employee-manager structure.

Model Setup

class Employee < ApplicationRecord
  belongs_to :manager, class_name: 'Employee', optional: true
  has_many :subordinates, class_name: 'Employee', foreign_key: 'manager_id'
end

Sample Data

idnamemanager_id
1AliceNULL
2Bob1
3Carol1
4Dave2

Query: Find Employees Who Report to Alice

Employee.joins(:manager).where(managers_employees: { name: 'Alice' })

Result:

idnamemanager_id
2Bob1
3Carol1

This query fetches employees who report to Alice (i.e., those where manager_id = 1).

3. Fetching Users with No Posts (LEFT JOIN with NULL check)

User.left_outer_joins(:posts).where(posts: { id: nil })

4. Counting Posts Per User

User.joins(:posts).group('users.id').count

Complex Queries in Active Record

1. Fetching Posts with the Most Comments

Post.joins(:comments)
    .group('posts.id')
    .order('COUNT(comments.id) DESC')
    .limit(1)

2. Fetching Posts with More than 5 Comments

Post.joins(:comments)
    .group(:id)
    .having('COUNT(comments.id) > ?', 5)

3. Finding Users Who Liked the Most Comments

User.joins(comments: :likes)
    .group('users.id')
    .select('users.id, users.name, COUNT(likes.id) AS likes_count')
    .order('likes_count DESC')
    .limit(1)

4. Fetching Posts Belonging to Multiple Categories

Post.joins(:categories).group('posts.id').having('COUNT(categories.id) > ?', 1)

5. Fetching the Last Comment of Each Post

Comment.select('DISTINCT ON (post_id) *').order('post_id, created_at DESC')

6. Fetching Users Who Haven’t Commented on a Specific Post

User.where.not(id: Comment.where(post_id: 10).select(:user_id))

7. Fetching Users Who Have Commented on Every Post

User.joins(:comments).group(:id).having('COUNT(DISTINCT comments.post_id) = ?', Post.count)

8. Finding Posts With No Comments

Post.left_outer_joins(:comments).where(comments: { id: nil })

9. Fetching the User Who Created the Most Posts

User.joins(:posts)
    .group('users.id')
    .select('users.id, users.name, COUNT(posts.id) AS post_count')
    .order('post_count DESC')
    .limit(1)

10. Fetching the Most Liked Comment

Comment.joins(:likes)
    .group('comments.id')
    .order('COUNT(likes.id) DESC')
    .limit(1)

11. Fetching Comments with More than 3 Likes and Their Associated Posts

Comment.joins(:likes, :post)
    .group('comments.id', 'posts.id')
    .having('COUNT(likes.id) > ?', 3)

12. Finding Users Who Haven’t Liked Any Comments

User.left_outer_joins(:likes).where(likes: { id: nil })

13. Fetching Users, Their Posts, and the Count of Comments on Each Post

User.joins(posts: :comments)
    .group('users.id', 'posts.id')
    .select('users.id, users.name, posts.id AS post_id, COUNT(comments.id) AS comment_count')
    .order('comment_count DESC')

Importance of inverse_of in Model Associations

What is inverse_of?

The inverse_of option in Active Record associations helps Rails correctly link objects in memory, avoiding unnecessary database queries and ensuring bidirectional association consistency.

Example Usage

class User < ApplicationRecord
  has_many :posts, inverse_of: :user
end

class Post < ApplicationRecord
  belongs_to :user, inverse_of: :posts
end

Why Use inverse_of?

  • Performance Optimization: Prevents extra queries by using already loaded objects.
  • Ensures Data Consistency: Updates associations without additional database fetches.
  • Enables Nested Attributes: Helps when using accepts_nested_attributes_for.

Example:

user = User.new(name: 'Alice')
post = user.posts.build(title: 'First Post')
post.user == user  # True without needing an additional query

Best Practices to use in Rails Projects

1. Using Scopes for Readability

class Post < ApplicationRecord
  scope :recent, -> { order(created_at: :desc) }
end

Post.recent.limit(10)  # Fetch recent posts

2. Using find_each for Large Datasets

User.find_each(batch_size: 100) do |user|
  puts user.email
end

3. Avoiding SELECT * for Performance

User.select(:id, :name).load

4. Avoiding N+1 Queries with includes

Post.includes(:comments).each do |post|
  puts post.comments.count
end


Conclusion

Mastering Active Record queries is essential for writing performant and maintainable Rails applications. By using joins, scopes, batch processing, and eager loading, you can write clean and efficient queries that scale well.

Do you have any favorite Active Record query tricks? Share them in the comments!

Mastering 𖡎 Ruby: Exploring Syntax Sugar, Operators, and Useful Methods

Ruby is known for its elegant and expressive syntax, making it one of the most enjoyable programming languages to work with. In this blog post, we will explore some interesting aspects of Ruby, including syntax sugar, operator methods, useful array operations, and handy built-in methods.


Ruby Syntax Sugar

Ruby provides several shorthand notations that enhance code readability and reduce verbosity. Let’s explore some useful syntax sugar techniques.

Shorthand for Array and Symbol Arrays

Instead of manually writing arrays of strings or symbols, Ruby provides %w and %i shortcuts.

words = %w[one two three]   # => ["one", "two", "three"]
symbols = %i[one two three] # => [:one, :two, :three]

These notations also support interpolation:

prefix = 'item'
words = %W[#{prefix}_one #{prefix}_two #{prefix}_three]  # => ["item_one", "item_two", "item_three"]
symbols = %I[#{prefix}_one #{prefix}_two #{prefix}_three] # => [:item_one, :item_two, :item_three]

Shorthand for Mapping and Selecting Elements

Instead of using map with blocks, we can use &: shorthand.

strings = %w[one two three]
upcased = strings.map(&:upcase) # => ["ONE", "TWO", "THREE"]

This is equivalent to:

strings.map { |str| str.upcase }


Operator Method Calls

Many mathematical and array operations in Ruby are actually method calls, thanks to Ruby’s message-passing model.

puts 1.+(2)  # => 3 (Same as 1 + 2)

Array Indexing and Append Operations

words = %w[zero one two three four]
puts words.[](2)   # => "two" (Equivalent to words[2])
words.<<('five')   # => ["zero", "one", "two", "three", "four", "five"] (Equivalent to words << "five")

Avoiding Dot Notation with Operators

# Bad practice
num = 10
puts num.+(5) # Avoid this syntax

# Good practice
puts num + 5  # Preferred

Using a linter like rubocop can help enforce best practices in Ruby code.


none? Method in Ruby Arrays

The none? method checks if none of the elements in an array satisfy a given condition.

numbers = [1, 2, 3, 4, 5]
puts numbers.none? { |n| n > 10 }  # => true (None are greater than 10)
puts numbers.none?(&:odd?)          # => false (Some numbers are odd)

This method is useful for quickly asserting that an array does not contain specific values.


Exponent Operator (**) and XOR Operator (^)

Exponentiation

Ruby uses ** for exponentiation instead of ^ (which performs a bitwise XOR operation).

puts 2 ** 3  # => 8 (2 raised to the power of 3)
puts 10 ** 0.5 # => 3.162277660168379 (Square root of 10)

XOR Operator

In Ruby, ^ is used for bitwise XOR operations, which differ from exponentiation.

puts 5 ^ 3  # => 6 (Binary: 101 ^ 011 = 110)
puts 10 ^ 7 # => 13 (Binary: 1010 ^ 0111 = 1101)

This is useful in scenarios involving bitwise manipulations, such as cryptography or performance optimizations.


Safe Navigation Operator (&.)

Ruby provides the safe navigation operator (&.) to avoid NilClass errors when calling methods on potentially nil objects.

user = nil
puts user&.name  # => nil (Avoids NoMethodError)

This is helpful when dealing with uncertain data, such as API responses or optional attributes.


Useful String Methods: chop and find_all

chop: Removing the Last Character

The chop method removes the last character from a string, unlike chomp, which removes only the newline character.

str = "Hello!"
puts str.chop  # => "Hello"

find_all: Filtering Collections

The find_all method (alias of select) filters elements that match a condition.

numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.find_all(&:even?) # => [2, 4]

This is particularly useful for data processing tasks.


=~ Operator in Ruby

The =~ operator is used to match a string against a regular expression and returns the index of the first match or nil if no match is found.

puts "hello" =~ /e/  # => 1 (Index of 'e' in "hello")
puts "hello" =~ /z/  # => nil (No match found)

This operator is useful for quick pattern matching in strings.


.squish Method in Ruby

The .squish method is part of ActiveSupport (Rails) and removes leading, trailing, and extra spaces within a string.

require 'active_support/all'

text = "   Hello   World!  "
puts text.squish  # => "Hello World!"

This is particularly useful for cleaning up user input or text from external sources.


Conclusion

Ruby’s syntax sugar, operator methods, and built-in methods make code more readable, expressive, and powerful. By leveraging these features effectively, you can write clean, efficient, and maintainable Ruby code.

Enjoy Ruby! 🚀

Rails 🛤 DSLs Explained: How Ruby Makes Configuration Elegant

Ruby on Rails is known for its developer-friendly syntax and expressive code structure. One of the key reasons behind this elegance is its use of Domain-Specific Languages (DSLs). DSLs make Rails configurations, routes, and testing more intuitive by allowing developers to write code that reads like natural language.

In this blog post, we’ll explore what DSLs are, how Rails implements them, and why they make development in Rails both powerful and enjoyable.


What is a DSL?

A Domain-Specific Language (DSL) is a specialized language designed to solve problems in a specific domain. Unlike general-purpose languages (like Ruby or Java), a DSL provides a more concise and readable syntax for a particular task.

Two types of DSLs exist:

  • Internal DSLs: Written using an existing programming language’s syntax (e.g., Rails DSLs in Ruby).
  • External DSLs: Separate from the host language and require a custom parser (e.g., SQL, Regular Expressions).

Rails uses Internal DSLs to simplify web development. Let’s explore some core DSLs in Rails and how they work under the hood.


1. Routes in Rails: A Classic Example of DSL

In config/routes.rb, Rails provides a DSL to define application routes in a clear and structured way.

Example:

Rails.application.routes.draw do
  resources :users do
    resources :posts
  end

  get '/about', to: 'pages#about'
  root 'home#index'
end

How Does This Work?

  • resources :users automatically generates RESTful routes for UsersController.
  • get '/about', to: 'pages#about' maps a GET request to the about action in PagesController.
  • root 'home#index' sets the default landing page.

Why Use a DSL for Routes?

  • Concise & Readable: Avoids manually defining each route.
  • Expressive Syntax: Reads like a structured list of instructions.
  • Reduces Boilerplate Code: Automates RESTful route creation.

Under the hood, Rails uses metaprogramming to convert this DSL into actual Ruby methods that map HTTP requests to controllers.


2. Configuration DSL in Rails: config/environments/development.rb

Rails also provides a DSL for application configuration using Rails.application.configure.

Example:

Rails.application.configure do
  config.cache_classes = false
  config.eager_load = false
  config.consider_all_requests_local = true
end

What is config Here?

  • config is an instance of Rails::Application::Configuration, a special Ruby object that stores settings.
  • The configure block modifies application settings dynamically using method calls.

Why a DSL for Configuration?

  • Expressiveness: Instead of setting key-value pairs in a hash, we use method calls (config.cache_classes = false).
  • Customization: Each environment (development, test, production) has its own configuration file.
  • Readability: Makes it easy to understand and modify settings.

3. RSpec’s describe Method: A DSL for Testing

RSpec, the popular testing framework for Ruby, provides a DSL for writing tests.

Example:

describe User do
  it "has a valid factory" do
    user = FactoryBot.create(:user)
    expect(user).to be_valid
  end
end

How Does This Work?

  • describe User do ... end defines a test suite for the User model.
  • it "has a valid factory" do ... end describes an individual test case.
  • expect(user).to be_valid checks if the user instance is valid.

Under the hood, describe is a method that creates a structured test suite dynamically.

Why Use a DSL for Testing?

  • Improves Readability: Tests read like English sentences.
  • Encapsulates Test Logic: Eliminates boilerplate setup code.
  • Encourages Behavior-Driven Development (BDD).

4. Defining Methods Dynamically: ActiveSupport::Concern

Rails extends DSL capabilities with ActiveSupport::Concern, which allows modular mixins in models and controllers.

Example:

module Trackable
  extend ActiveSupport::Concern

  included do
    before_save :track_changes
  end

  private
  def track_changes
    puts "Tracking changes!"
  end
end

class User < ApplicationRecord
  include Trackable
end

How This Works:

  • included do ... end executes code when the module is included in a class.
  • before_save :track_changes hooks into the Rails lifecycle to run before saving a record.

Why a DSL for Mixins?

  • Encapsulation: Keeps related logic together.
  • Reusability: Can be included in multiple models.
  • Cleaner Code: Removes redundant callbacks in models.

Conclusion: Why Rails Embraces DSLs

DSLs in Rails make the framework expressive, flexible, and developer-friendly. They provide:

Concise syntax (reducing boilerplate code). ✅ Readability (code reads like natural language). ✅ Powerful abstractions (simplifying complex tasks). ✅ Customization (tailoring behavior dynamically).

By leveraging DSLs, Rails makes web development intuitive, allowing developers to focus on building great applications rather than writing repetitive code.

So next time you’re defining routes, configuring settings, or writing tests in Rails—remember, you’re using DSLs that make your life easier!

Enjoy Ruby 🚀