Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Migrations: How to Create, Run and Rollback Migrations

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13 Migrations: How to Create, Run and Rollback Migrations

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 Migrations: How to Create, Run and Rollback Migrations

To create a migration in Laravel 13, run php artisan make:migration create_posts_table, define the schema in the up() method and the reverse in down(). Run pending migrations with php artisan migrate and undo the last batch with php artisan migrate:rollback. Use --step to roll back a specific number of migrations.

Migrations are version control for your schema. In team projects I treat them like code: every schema change is a migration, every migration is reviewed, and no one edits the production database by hand.

Prerequisites

  • Laravel 13.x on PHP 8.3 or newer.
  • Database credentials in .env. New Laravel projects default to SQLite, which needs no server.

Step 1: Create a migration

Code
php artisan make:migration create_posts_table

Laravel reads the name to guess the table. Names like create_posts_table generate a Schema::create() stub, and names like add_status_to_posts_table generate a Schema::table() stub. The file lands in database/migrations with a timestamp prefix that decides the run order.

You can also create a model and its migration together with php artisan make:model Post -m.

Step 2: Write the up and down methods

Laravel 13 uses anonymous migration classes, so file names never clash:

JavaScript
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

foreignId('user_id')->constrained() creates an unsigned big integer column and a foreign key to users.id, guessing the table from the column name.

Step 3: Run the migrations

Code
php artisan migrate

Laravel records every executed migration in the migrations table, together with a batch number. All migrations run by one migrate call share a batch. Useful companions:

YAML
# Which migrations have run, and in which batch?
php artisan migrate:status

# Show the SQL without executing it
php artisan migrate --pretend

# Give each migration its own batch, so you can roll back one at a time
php artisan migrate --step

How do I roll back a migration in Laravel 13?

Rollback runs the down() methods in reverse order:

SQL
# Undo the last batch
php artisan migrate:rollback

# Undo the last 3 migrations, regardless of batch
php artisan migrate:rollback --step=3

# Undo one specific batch
php artisan migrate:rollback --batch=4

# Preview the rollback SQL
php artisan migrate:rollback --pretend

What is the difference between migrate:reset, refresh and fresh?

  • migrate:reset rolls back every migration by calling all down() methods.
  • migrate:refresh resets and then runs everything again. Add --seed to run seeders afterwards.
  • migrate:fresh drops all tables without calling down(), then migrates. It is the fastest way to rebuild a local database, and also works when a down() method is broken.

All three destroy data. Use them locally or in tests, never on a production database you care about.

How do I add a column to an existing table?

Never edit a migration that has already run in another environment. Create a new one instead:

Code
php artisan make:migration add_status_to_posts_table
JavaScript
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->string('status', 20)->default('draft')->after('slug')->index();
        });
    }

    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropIndex(['status']);
            $table->dropColumn('status');
        });
    }
};

The after() modifier only affects MySQL and MariaDB column order. To change an existing column, call ->change() and repeat every modifier you want to keep, because missing attributes are dropped.

Running migrations in production

Code
php artisan migrate --force --isolated

In production Laravel asks for confirmation before destructive commands; --force skips the prompt for deploy scripts. --isolated takes an atomic lock through your cache driver, so two servers deploying at the same time do not run the same migrations twice. It needs a shared cache store such as Redis, database or Memcached.

When the migrations folder grows large, php artisan schema:dump --prune writes the current schema to database/schema and removes the old migration files. Laravel loads that dump first on a fresh database.

How should I name and organize migrations?

Descriptive names pay off months later when you scan migrate:status. I follow three habits: one logical change per migration, verbs that describe the change (create_, add_..._to_, drop_..._from_, rename_), and data fixes in their own migration, separate from schema changes, so a failed backfill does not block the structural change. Keep each migration small enough that its down() method is obvious. If two developers create migrations on different branches, the timestamps decide the order after merging, so avoid migrations that depend on another branch's unmerged table.

Common mistakes

  • Editing old migrations. Colleagues and servers that already ran them will never see the change.
  • Empty down() methods. Rollbacks then succeed silently without undoing anything.
  • Dropping a column before its index or foreign key. Drop the constraint first, for example $table->dropForeign(['user_id']), or use dropConstrainedForeignId('user_id').
  • Using models inside migrations. Models change over time; old migrations then break. Use the query builder for data fixes.
  • Running migrate:fresh against the wrong database. Check DB_DATABASE before running it.

How to verify it works

Run php artisan migrate:status and confirm the new migration shows "Ran". Inspect the table with php artisan db:table posts, which lists columns, indexes and foreign keys. Then prove the migration is reversible: run php artisan migrate:rollback --step=1 followed by php artisan migrate. If both succeed, your down() method works.

FAQ

How do I run a single migration file?

Use the path option: php artisan migrate --path=database/migrations/2026_11_11_000000_create_posts_table.php.

Can I run migrations on another connection?

Yes. Pass --database=connection_name, or set a $connection property on the migration class.

Do tests need migrations?

Use the RefreshDatabase trait in your tests. It migrates the test database once and wraps each test in a transaction.

References

Article Index & Topic Tags
LaravelDatabaseArtisanMigrations

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.