Migration in laravel: Master Your Database Migrations with Ease
At its core, a migration in Laravel is just a PHP file. But what it represents is huge: it's version control for your database. These files let you define and modify your database schema using code, creating a clear, collaborative, and predictable way to manage its structure. This means no more manual SQL edits and a consistent database across every developer's machine and your production server.
Why Laravel Migrations Are Essential for Your Database

Imagine trying to build an application without Git. It would be chaos. That's what managing a database without migrations feels like. Instead of passing around .sql files or having team members manually tweak tables, you just create simple PHP files.
These files describe exactly what you want to change—maybe adding a new users table or a last_login_at column. When you run the migration, Laravel translates your PHP into the right SQL for your database. It’s a simple concept that completely changes how you work.
The Power of Programmatic Schemas
The real magic of using a migration in Laravel is how it locks your database schema in sync with your codebase. As you build new features, you create migrations right alongside the code. When it's time to deploy, you just run the migrations, and your database instantly matches what the new code expects.
This brings some serious advantages to the table:
- Effortless Team Collaboration: Migrations ensure every developer has the exact same database structure. When someone pulls the latest code, they just run
php artisan migrate, and their local database is immediately up to date. No more "it works on my machine" issues caused by database drift. - Rock-Solid Deployments: By baking migrations into your deployment pipeline, you automate database updates. This dramatically cuts down on the human error that’s so common with manual changes, especially on a live production server.
- Simple Reversibility: Every migration has two jobs. The
up()method applies the change, and thedown()method reverses it. If a deployment goes sideways, you can instantly roll back the last database change without scrambling to fix things manually.
I once saw a distributed team dodge a major deployment bullet thanks to migrations. A developer in one time zone added a
NOT NULLcolumn, while another was prepping a release. Because that change was captured in a migration file and committed to Git, the conflict was flagged before it went live, preventing a crash.
Ultimately, migrations become the single source of truth for how your database is built. They turn a potentially chaotic process into something repeatable, safe, and efficient. If you want to get the most out of them, brushing up on some database design best practices can make writing effective migrations even more intuitive.
Generating Your First Migration with Artisan

Alright, let's get our hands dirty. Every change to your database schema starts with a simple command using Artisan, Laravel's built-in command-line tool. These commands generate special PHP files that live inside your database/migrations directory, acting as a version-controlled history of your database.
To get started, pop open your terminal and run this command to create a migration for a new products table:
php artisan make:migration create_products_table
Notice the naming convention here: create_products_table. This isn't just for show. Laravel is smart enough to parse this name and automatically generate a migration file with the boilerplate code needed to create a new table. It saves you a few keystrokes and keeps things consistent.
A Quick Guide to Artisan Commands
As you work with migrations, you'll find yourself using a core set of Artisan commands over and over. Here’s a quick reference table to keep handy.
Essential Artisan Migration Commands
| Command | Description |
|---|---|
php artisan migrate |
Runs all pending migrations that haven't been executed yet. |
php artisan migrate:rollback |
Reverts the last batch of migrations that were run. |
php artisan migrate:fresh |
Drops all tables and re-runs all migrations from scratch. |
php artisan migrate:refresh |
Rolls back all migrations and then runs them all again. |
php artisan migrate:status |
Shows the status of each migration (whether it has been run or not). |
php artisan make:migration <name> |
Creates a new migration file with the specified name. |
This table covers the essentials you'll need for most day-to-day database work.
Why Naming Migrations Matters
I can't stress this enough: the name of your migration file is your first line of documentation. A good name tells you exactly what the file does without you ever having to open it. This becomes incredibly important on larger teams or when you return to a project after six months.
For example, what if you need to add a sku column to the products table? A vague name like update_products is useless. Instead, be specific:
php artisan make:migration add_sku_to_products_table
Just like before, Laravel intelligently figures out you want to modify an existing table and gives you a Schema::table() block instead of Schema::create(). It’s a small detail that makes a big difference.
Pro Tip: Think of your migration names as commit messages. A clear, descriptive name like
add_soft_deletes_to_users_tableis a gift to your future self and your colleagues. A name likeuser_updates_v2just creates confusion and technical debt.
Inside the Migration File: Up and Down
Once you run make:migration, a new file appears in database/migrations. If you open the one we just created for our products table, you’ll find a class with two critical methods: up() and down().
The
up()method is where you define the "forward" change. This is what runs when you executephp artisan migrate. It’s where you’ll add your code to create tables, add columns, or define new indexes.The
down()method is your safety net. It contains the code to perfectly reverse whatever theup()method did. If you create a table inup(), you must drop that table indown(). This is what allows you to roll back changes safely.
For our create_products_table migration, the up() method comes pre-filled and ready to go:
public function up(): void { Schema::create('products', function (Blueprint $table) { $table->id(); // You'll add more columns like name, price, etc. here $table->timestamps(); }); }
On the flip side, the down() method ensures this action is completely reversible, giving you a way to undo the change cleanly.
public function down(): void { Schema::dropIfExists('products'); }
This up() and down() structure is the heart of what makes migrations so reliable. It gives you the confidence to make incremental changes to your database, knowing you always have a clear path to roll back if needed. Getting comfortable with this workflow is the first step to managing any database schema, no matter how complex it gets.
Building Your Schema and Modifying Tables

Alright, you've got your new migration file. This is where the real work begins—sculpting your database structure using Laravel's Schema facade. Instead of writing raw SQL, you get to define your tables and columns with clean, expressive PHP. It feels less like coding and more like describing your database structure.
Inside your migration's up() method, the Schema::create() method is your starting point for a new table. Laravel hands you a Blueprint object, which is essentially your toolkit for adding all the columns you need.
Defining Your Columns
The Blueprint object comes loaded with methods for just about any column type you can imagine, from the basics to highly specialized fields. You're not just stuck with text and numbers; the schema builder is built for modern applications.
Here are a few of the most common types you'll use constantly:
string('name', 100): Creates aVARCHARcolumn, perfect for titles or short text.text('description'): The go-to for longer content, like a blog post body.integer('price'): A standardINTEGERcolumn for whole numbers.boolean('is_published'): Creates aBOOLEANfield, which is great for on/off flags.json('options'): AJSONcolumn, incredibly useful for storing flexible or unstructured data.
Of course, this is just scratching the surface. Laravel has dedicated methods for timestamps, auto-incrementing IDs ($table->id()), and many others. The trick is picking the right tool for the job.
Using Column Modifiers for Control
Just defining a column's type isn't enough. The real power comes from chaining column modifiers to add constraints and attributes. This is how you enforce data integrity right at the database level, which is always a better strategy than relying only on application-level validation.
By applying constraints like
unique()andnullable()directly in your migrations, you build a much more resilient schema. You're preventing bad data from ever getting into your system in the first place.
Imagine you're building a users table. You'd definitely want to ensure every email is unique, and maybe a user's bio is optional. You can lock this down with just a couple of extra method calls.
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique(); // Enforces email uniqueness at the DB level
$table->text('bio')->nullable(); // Allows this column to be empty
$table->integer('login_count')->default(0); // Sets a default value
$table->timestamps();
});
Without writing a single line of SQL, we've created a solid foundation for our users table.
Modifying Existing Tables
Your application will change, and so will its database needs. You'll inevitably have to add a new column, tweak an existing one, or even drop one entirely. Whatever you do, never edit an old migration file. This is a cardinal sin in the Laravel world. Instead, you create a new migration for every single change.
To modify a table, you'll use Schema::table(). Let's say we need to add a status column to a products table. First, generate the migration:
php artisan make:migration add_status_to_products_table --table=products
Then, inside the new migration file, you define the change in the up() method and, crucially, how to reverse it in the down() method.
// In the up() method Schema::table('products', function (Blueprint $table) { $table->string('status')->default('draft')->after('name'); });
// In the down() method
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('status');
});
This approach keeps your database history perfectly linear and understandable. Renaming or dropping columns is just as straightforward with methods like $table->renameColumn() and $table->dropColumn().
The Importance of Indexes and Foreign Keys
For any serious application, performance and data integrity are non-negotiable. This is where indexes and foreign keys come in. Indexes make your read queries faster, while foreign keys enforce relationships between your tables. If you want a deeper understanding, our guide on how to work with a foreign key in MySQL is a great resource.
For example, let's link a products table to a categories table. Laravel gives you an incredibly elegant way to do this:
$table->foreignId('category_id')->constrained()->onDelete('cascade');
This single line of code is a powerhouse. It creates the category_id column, adds a foreign key constraint pointing to the id on the categories table, and even tells the database to delete all related products if their parent category is deleted. It’s a beautifully concise way to build relational integrity.
Running Migrations, Seeding, and Advanced Techniques
So you’ve written your migration files. Now comes the fun part: actually making those changes to your database. This is where your carefully crafted PHP classes become real, tangible tables and columns. The main command you'll be using is refreshingly simple.
php artisan migrate
When you fire off this command, Laravel gets to work. It consults its own internal migrations table to see what it has already run, compares that against your migration files, and then executes the up() method on any new ones. It’s smart enough to run them in chronological order, ensuring your database schema always matches your application's code.
Of course, we're all human. You'll run a migration and immediately realize you misspelled a column name. It happens. This is exactly what the down() method is for. To undo the last set of migrations you ran, just use the rollback command.
php artisan migrate:rollback
Think of this as your "undo" button for the database schema. It finds the last "batch" of migrations that were executed and runs their down() methods, neatly reverting the changes.
Populating Your Database with Seeding
An empty database is pretty useless during development. Clicking around and manually adding test users or products every time you reset your database gets old, fast. That's precisely the problem Laravel’s database seeding was built to solve.
Seeders are simple classes that exist for one purpose: to stuff your database with sample or default data. You can create one easily with an Artisan command:
php artisan make:seeder UserSeeder
This command creates a new file in database/seeders. Inside the seeder's run() method, you can go wild with model factories or the query builder to create all the data you need. For example, you might generate 50 fake users to make your user list look populated.
Pro Tip: For a truly fresh development environment, you can combine migrations and seeding into one smooth operation. The
--seedflag is your best friend here.
php artisan migrate:fresh --seed
This command is a lifesaver. I use it constantly. It drops every table, runs all your migrations from scratch, and then runs your seeders. In one command, you get a perfectly clean, predictable database state, ready for you to start coding.
Handling Advanced Migration Scenarios
As your project gets bigger and older, you'll run into some more complex situations. Your migration history might get long, or you might need to do something that Laravel's standard Schema builder just can't handle. Here are a couple of techniques I've found essential for managing that complexity.
Using Raw SQL Queries
Let's be honest, Laravel's Schema builder is fantastic and probably covers 95% of what you'll ever need to do. But every now and then, you hit a wall. Maybe you need to create a complex, database-specific view or add a stored procedure. For those rare cases, you can drop down to raw SQL right inside your migration.
The DB::unprepared() method is your escape hatch. It lets you run any SQL you want.
// In up() method DB::unprepared('CREATE VIEW popular_products AS SELECT * FROM products WHERE view_count > 1000');
// In down() method DB::unprepared('DROP VIEW IF EXISTS popular_products'); This is perfect because your custom SQL is still version-controlled and part of the normal migration flow, so you don't have to manage separate SQL scripts.
Squashing Migrations
After a few years, a successful project can have hundreds, if not thousands, of migration files. This can slow down your test runs and makes the database/migrations directory a nightmare to scroll through. Thankfully, Laravel has a brilliant solution: squashing.
php artisan schema:dump --prune
This command is pure magic. It connects to your database, figures out the final structure of all your tables, and dumps it into a single .sql file. The --prune flag then goes a step further and deletes all the old migration files it just consolidated.
From that point on, when you run php artisan migrate on a new machine, Laravel will first load that single SQL file to build the entire schema in a flash. Then, it will only run the new migrations you created after you made the dump. It’s an incredible way to clean house and speed up new environment setups.
Handling Migrations in a Production Environment

Pushing a migration in Laravel to a live server is where things get serious. This isn't your local sandbox anymore; a mistake here can cause real downtime and a very stressful afternoon. The goal is to make database updates a boring, predictable part of your deployment.
The best way I’ve found to do this is by making migrations a core part of your deployment script or CI/CD pipeline. Every time you push new code, your script should automatically run php artisan migrate. This simple step ensures your database schema never falls out of sync with what your application code is expecting.
Strategies for Zero-Downtime Deployments
Of course, the holy grail is a zero-downtime deployment, where users don't even notice an update is happening. To pull this off, your schema changes have to be backward-compatible. This is a critical concept: your old code must run flawlessly against the new database schema, and the new code must work with the old one.
Let's walk through a common scenario: adding a new, required non-nullable column. If you just add it in one go, your old code will crash the moment it tries to create a new record without that column.
Here’s a much safer, two-step deployment dance:
- First Deployment: Your migration adds the new column, but you make sure it's
nullable(). Deploy this. The old code doesn't care about the new column, and the new code can handle cases where it might be null. No-drama. - Second Deployment: Now, you can update your application code to start populating that new column. Once that's live and stable, you can deploy a second migration that finally changes the column to be non-nullable (
->nullable(false)).
This phased approach completely sidesteps those "column not found" or "cannot be null" errors that can take a site down in a flash. As you get more comfortable with these, it's worth looking into broader data migration best practices to round out your strategy.
A crucial pro-tip:
php artisan migrateshould run at the very beginning of your post-deployment script. If a migration fails, the entire deployment must stop and roll back before your new code is activated. This prevents the classic mismatch where new code tries to talk to a database schema that doesn't exist yet.
Troubleshooting Production Migration Failures
Even the best-laid plans can go awry. A migration can fail halfway through, leaving your database in a dreaded "in-between" state. The most common error you’ll see is the infamous 'Base table or view already exists' message.
This usually happens when a migration creates a new table but then fails on a later step before it can log its own success in the migrations table. When you try to run migrate again, it tries to create the same table and immediately errors out.
Whatever you do, don't just log in and manually delete the table from the database! The proper fix is to connect to your production database and manually insert a record for the failed file into the migrations table. This essentially tells Laravel, "Hey, this one has already run," allowing the migration process to pick up where it left off. For tracking down the root cause of these issues, getting familiar with how to check the log file in Laravel is an invaluable skill.
A Few Common Questions About Laravel Migrations
As you get your hands dirty with Laravel migrations, a few questions tend to pop up again and again. Whether you're just starting out or running into a tricky database scenario, these are some of the most common hurdles developers face—and how to clear them.
Can I Edit an Old Migration File?
It’s a classic temptation: you spot a typo in a migration you already ran and think, "I'll just edit the file." My advice? Almost never do this.
Once a migration has been run, especially in a shared project or on a production server, that file should be treated as set in stone. Editing it directly means your local database schema no longer matches the history your team sees in version control. This is a recipe for headaches and major inconsistencies down the road.
The proper way to fix it is to create a new migration that applies the change. If you added a column with the wrong name, for instance, you'd generate a new migration file just to rename that column.
The only real exception is if you've only run the migration on your local machine and haven't pushed the code anywhere. If you're in that specific, isolated situation, you can safely roll it back (php artisan migrate:rollback), edit the file, and then run it again.
Think of your
database/migrationsfolder as a historical ledger for your database. You wouldn't just erase a past transaction; you'd add a new one to correct it. The same principle applies here.
How Do I Add a Column to an Existing Table?
Adding a new column to a table you created earlier is a fundamental part of evolving an application. To keep your database history clean and version-controlled, you should always generate a new migration specifically for this task.
The key is to use the --table flag so Artisan knows you're modifying an existing table, not creating a new one.
php artisan make:migration add_is_featured_to_products_table --table=products
Laravel is smart enough to see that flag and will pre-fill the new migration file with a Schema::table() block. From there, you just need to define your change in the up() method and, just as importantly, how to reverse it in the down() method.
- In
up():Schema::table('products', function (Blueprint $table) { $table->boolean('is_featured')->default(false); }); - In
down():Schema::table('products', function (Blueprint $table) { $table->dropColumn('is_featured'); });
This keeps every schema change perfectly self-contained and reversible, which is exactly what you want.
What Is the Difference Between Refresh and Fresh?
Two Artisan commands that sound nearly identical but have very different—and destructive—outcomes are migrate:refresh and migrate:fresh. Knowing when to use each one is critical.
php artisan migrate:refresh: This command first runs thedown()method for every single one of your migrations, rolling them all back. Once that's done, it runs all theup()methods again to rebuild your schema from scratch. It’s a good way to test that yourdown()methods are working correctly.php artisan migrate:fresh: This one is more of a sledgehammer. It drops all tables from your database immediately, ignoring thedown()methods entirely. It then runs all your migrations from the very beginning.
So, which should you use? migrate:fresh is faster and gives you a truly clean slate, which is perfect for the early days of development when you're still figuring out your schema and don't care about the data. migrate:refresh, on the other hand, is a bit more methodical and helps ensure your migrations are properly reversible.