Ruby on Rails vs. Laravel
Two frameworks have shaped modern web development more than any others: Ruby on Rails brought convention over configuration to the masses, while Laravel made PHP development actually enjoyable. Both promise rapid development and clean code, but they achieve it through completely different approaches.
Ruby on Rails revolutionized web development by proving that convention beats configuration every time. Instead of writing XML files and wrestling with directory structures, you follow Rails' opinions and get working web applications in minutes. It's the framework that launched a thousand startups and convinced developers that productivity matters more than flexibility.
Laravel rescued PHP from its messy reputation by borrowing the best ideas from Rails and adding modern PHP elegance. Eloquent ORM reads like English, Artisan commands handle tedious tasks, and the whole ecosystem feels designed by someone who actually builds web applications for a living.
Your choice between them determines your development speed, deployment options, and long-term maintenance strategy. Here's how to pick the right one for your project.
What is Ruby on Rails?
Web development sucked in the early 2000s. Java developers wrote XML configuration files longer than their actual code. PHP developers mixed database queries with HTML templates. Everyone reinvented the same boring patterns over and over.
David Heinemeier Hansson was building Basecamp when he got tired of this nonsense. He extracted the common patterns into a framework and called it Ruby on Rails. The core insight was simple: most web applications need the same things, so why not build those patterns into the framework itself?
Rails made two revolutionary bets. First, that developers are happier when the framework makes reasonable decisions for them. Second, that convention over configuration actually works if you pick good conventions. The result changed how an entire generation thinks about web development.
What is Laravel?
PHP had an image problem by 2011. Developers associated it with spaghetti code, security vulnerabilities, and WordPress plugins that broke everything. The language itself was improving, but the frameworks still felt clunky and enterprise-y.
Taylor Otwell was working with existing PHP frameworks when he realized they were solving the wrong problems. Instead of copying Java patterns, why not learn from Rails' success and build something that actually felt good to use? Laravel became PHP's answer to Rails, but with its own personality.
The breakthrough was making PHP feel modern without losing its deployment advantages. You get Rails-style elegance with PHP's ubiquitous hosting support. It's what convinced thousands of developers that PHP could be a pleasure to work with.
Framework comparison
These frameworks share the same goals but take different paths to get there. Your choice shapes everything from development speed to server requirements.
Aspect | Ruby on Rails | Laravel |
---|---|---|
Language | Ruby | PHP |
Learning Curve | Steep but rewarding | Gentle with clear docs |
Development Speed | Very fast with conventions | Fast with Artisan tools |
Hosting | Requires Ruby-friendly hosts | Works everywhere PHP does |
Community | Passionate and opinionated | Large and welcoming |
Philosophy | Convention over configuration | Elegant syntax with flexibility |
Database | Active Record pattern | Eloquent ORM with relationships |
Testing | Built-in with strong culture | PHPUnit integration |
Background Jobs | Sidekiq/Resque with Redis | Queues with multiple drivers |
Asset Pipeline | Sprockets/Webpacker | Mix with Webpack |
The choice often comes down to your team's background and deployment constraints. If you love Ruby and don't mind Ruby-specific hosting, Rails offers unmatched productivity. If you need broad hosting support and your team knows PHP, Laravel provides similar developer happiness with more deployment options.
Getting started
let's begin by analyzing how each framework approaches development from the very first command.
Rails assumes you want to build a full web application:
gem install rails
rails new blog_app
cd blog_app && rails server
Rails generates a complete application structure with opinions about everything:
app/
├── controllers/
├── models/
├── views/
└── helpers/
config/
├── routes.rb
├── database.yml
└── application.rb
Your first feature takes one command:
rails generate scaffold Post title:string content:text
rails db:migrate
This creates a complete CRUD interface with views, controller actions, and database migration. Rails assumes you want standard web app functionality and gives it to you instantly. The scaffold includes HTML forms, validation, and even basic styling.
Laravel focuses on developer experience from the first command:
composer create-project laravel/laravel blog_app
cd blog_app && php artisan serve
Laravel's structure feels familiar to most web developers:
app/
├── Http/Controllers/
├── Models/
└── Services/
resources/
├── views/
└── assets/
routes/
├── web.php
└── api.php
Building your first feature involves multiple focused commands:
php artisan make:model Post -mc
php artisan make:resource PostResource
php artisan migrate
Laravel separates concerns more explicitly. You get a model, migration, and controller, but you write the routes and views yourself. This gives you more control over the final structure but requires more decisions upfront.
Database integration
Once you've got your project running, you'll need to work with data. Here's where Rails and Laravel also differ.
Rails uses Active Record, where models are your database interface:
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
validates :title, presence: true, length: { minimum: 5 }
validates :content, presence: true
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
def excerpt(length = 150)
content.truncate(length)
end
end
# Usage feels natural
@posts = Post.published.recent.includes(:user)
@post = Post.find(params[:id])
@post.update!(title: "New Title")
Active Record combines your data model, database queries, validations, and business logic in one place. Call Post.published
and Rails builds the SQL query. Access @post.comments
and Rails loads the relationship automatically. The magic makes simple cases trivial but can make complex queries harder to understand.
Rails migrations define schema changes with Ruby code:
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :content
t.references :user, null: false, foreign_key: true
t.boolean :published, default: false
t.timestamps
end
add_index :posts, [:published, :created_at]
end
end
Laravel uses Eloquent ORM with a similar active record pattern:
class Post extends Model
{
protected $fillable = ['title', 'content', 'user_id', 'published'];
protected $casts = [
'published' => 'boolean',
'published_at' => 'datetime'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function scopePublished($query)
{
return $query->where('published', true);
}
public function getExcerptAttribute()
{
return Str::limit($this->content, 150);
}
}
// Usage is similarly intuitive
$posts = Post::published()->with('user')->latest()->get();
$post = Post::findOrFail($id);
$post->update(['title' => 'New Title']);
Eloquent feels familiar to Rails developers but with PHP syntax. The $fillable
array protects against mass assignment. Relationships work through method names and conventions. Accessors like getExcerptAttribute
add computed properties to your models.
Laravel migrations use a similar fluent syntax:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->foreignId('user_id')->constrained();
$table->boolean('published')->default(false);
$table->timestamps();
$table->index(['published', 'created_at']);
});
Both approaches make database work feel natural, but Rails tends to hide more complexity while Laravel gives you slightly more explicit control over the underlying SQL.
Authentication and authorization
Now that you've seen how you can built some features, let's tackle user authentication. This is where you'll really see how differently these frameworks approach common problems.
Rails includes basic authentication building blocks but expects you to choose your approach:
# Gemfile
gem 'bcrypt'
# User model
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :password, length: { minimum: 6 }
end
# Application controller
class ApplicationController < ActionController::Base
before_action :authenticate_user!
private
def authenticate_user!
redirect_to login_path unless current_user
end
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
# Sessions controller
class SessionsController < ApplicationController
skip_before_action :authenticate_user!, only: [:new, :create]
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path
else
flash.now[:alert] = "Invalid email or password"
render :new
end
end
end
Rails gives you has_secure_password
and session management, but you build the authentication flow yourself. This means more code but complete control over the user experience. Many Rails apps use Devise gem for complete authentication systems, but the manual approach teaches you exactly how authentication works.
Laravel includes authentication out of the box with multiple approaches:
# Install authentication scaffolding
composer require laravel/ui
php artisan ui bootstrap --auth
php artisan migrate
This generates complete authentication views, controllers, and routes. For APIs, Laravel Sanctum provides token authentication:
// User model
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
}
// API authentication
class AuthController extends Controller
{
public function login(Request $request)
{
if (Auth::attempt($request->only('email', 'password'))) {
$token = auth()->user()->createToken('api-token')->plainTextToken;
return response()->json(['token' => $token]);
}
return response()->json(['error' => 'Invalid credentials'], 401);
}
}
// Protect routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
});
Laravel's approach gives you working authentication immediately but with less learning about the underlying mechanisms. The auth()
helper is available everywhere, and middleware handles route protection declaratively.
For authorization, Laravel uses policies that read like business rules:
class PostPolicy
{
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post)
{
return $user->id === $post->user_id || $user->isAdmin();
}
}
// Usage in controllers
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$post->update($request->validated());
return new PostResource($post);
}
Both frameworks handle authentication well, but Rails makes you understand the pieces while Laravel gives you working solutions immediately.
Testing strategies
You've built features and secured them. Now let's talk about making sure they actually work. Rails and Laravel take surprisingly different approaches to testing.
Rails has testing baked into it from day one:
# test/models/post_test.rb
class PostTest < ActiveSupport::TestCase
test "should validate presence of title" do
post = Post.new(content: "Test content")
assert_not post.valid?
assert_includes post.errors[:title], "can't be blank"
end
test "should create excerpt from content" do
post = posts(:long_post)
assert_equal "This is the beginning...", post.excerpt(25)
end
test "published scope returns only published posts" do
published = Post.published
assert published.all?(&:published?)
end
end
class PostsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:john)
@post = posts(:sample_post)
end
test "should get index" do
get posts_url
assert_response :success
assert_select 'h1', 'All Posts'
end
test "should create post when authenticated" do
sign_in @user
assert_difference('Post.count') do
post posts_url, params: {
post: { title: 'New Post', content: 'New content' }
}
end
assert_redirected_to post_url(Post.last)
end
end
Rails testing emphasizes integration tests that exercise the full stack. The assert_select
method validates HTML output. Fixtures provide consistent test data. The Rails community expects comprehensive test coverage, and many developers practice test-driven development.
Rails also includes system tests for full browser automation:
class PostsSystemTest < ApplicationSystemTestCase
test "creating a post" do
visit new_post_url
fill_in "Title", with: "System Test Post"
fill_in "Content", with: "This post was created by a system test"
click_on "Create Post"
assert_text "Post was successfully created"
assert_current_path post_path(Post.last)
end
end
Laravel includes testing support with PHPUnit and focuses on feature testing:
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_view_posts()
{
$posts = Post::factory(3)->create();
$response = $this->get('/posts');
$response->assertStatus(200)
->assertSeeText($posts->first()->title);
}
public function test_authenticated_user_can_create_post()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/posts', [
'title' => 'Test Post',
'content' => 'Test content for the post'
]);
$response->assertRedirect()
->assertSessionHas('success');
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
'user_id' => $user->id
]);
}
public function test_post_requires_authentication()
{
$response = $this->post('/posts', [
'title' => 'Unauthorized Post'
]);
$response->assertRedirect('/login');
}
}
Laravel testing focuses on HTTP interactions and database state. The RefreshDatabase
trait ensures clean test environments. Factory classes generate realistic test data with relationships. Laravel developers often test at the feature level rather than unit level.
For browser testing, Laravel includes Dusk:
class PostBrowserTest extends DuskTestCase
{
public function test_user_can_create_post()
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/posts/create')
->type('title', 'Browser Test Post')
->type('content', 'Created via browser automation')
->press('Create Post')
->assertPathIs('/posts')
->assertSee('Browser Test Post');
});
}
}
Both frameworks support comprehensive testing, but Rails culture emphasizes testing more heavily while Laravel focuses on making testing approachable for developers new to the practice.
Background processing
Your app is tested and ready, but what happens when users trigger tasks that take forever to complete? Let's look at how Rails and Laravel handle background jobs.
Rails traditionally uses Redis-backed job processors:
# Gemfile
gem 'sidekiq'
# Job class
class SendWelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.welcome_email(user).deliver_now
logger.info "Welcome email sent to #{user.email}"
rescue StandardError => e
logger.error "Failed to send welcome email: #{e.message}"
raise
end
end
# Usage in controllers
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
SendWelcomeEmailJob.perform_later(@user)
redirect_to @user, notice: 'Account created!'
else
render :new
end
end
end
# For recurring tasks
class CleanupJob < ApplicationJob
def perform
Post.where('created_at < ?', 1.month.ago)
.where(published: false)
.destroy_all
end
end
# Schedule with whenever gem
# config/schedule.rb
every 1.day, at: '2:00 am' do
runner "CleanupJob.perform_later"
end
Rails jobs are just Ruby classes with a perform
method. Sidekiq processes jobs in separate processes and provides a web UI for monitoring. The perform_later
method queues jobs asynchronously. Rails also includes good_job as a database-backed alternative to Redis.
Laravel includes job queues with multiple backend options:
// Job class
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function handle()
{
Mail::to($this->user->email)->send(new WelcomeMail($this->user));
Log::info("Welcome email sent to {$this->user->email}");
}
public function failed(Throwable $exception)
{
Log::error("Welcome email failed: {$exception->getMessage()}");
}
}
// Usage in controllers
class UserController extends Controller
{
public function store(Request $request)
{
$user = User::create($request->validated());
SendWelcomeEmail::dispatch($user);
return redirect()->route('users.show', $user)
->with('success', 'Account created!');
}
}
// Scheduled tasks
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->job(new CleanupOldPosts)->daily();
$schedule->call(function () {
Post::whereNull('published_at')
->where('created_at', '<', now()->subMonth())
->delete();
})->daily();
}
Laravel jobs implement ShouldQueue
and get automatic serialization. The framework supports database, Redis, and SQS backends. Horizon provides a dashboard for Redis queues. The scheduler runs through a single cron entry that executes php artisan schedule:run
every minute.
Both frameworks handle background processing well, but Rails typically requires more setup (Redis, Sidekiq) while Laravel works with just a database queue for simple cases.
Final thoughts
Both frameworks will help you build web applications quickly and maintainably. Rails offers slightly faster development if you embrace its conventions completely. Laravel provides similar productivity with more deployment flexibility and explicit control.
The honest answer is that both frameworks are mature, well-documented, and actively maintained. Your team's existing skills and infrastructure constraints matter more than the technical differences between them.
Try building the same simple blog application in both frameworks. You'll quickly discover which approach feels more natural for your brain and your project's requirements.