LessonsBuild a Blog → Part 2

Database Design & Migrations

Laravel
⏱ 25 min read🏗️ ProjectNot completed

In this lesson we'll create all the database migrations for our blog. Good database design is the foundation of any application.

Create All Migrations

Terminal
# Create models with migrations
php artisan make:model Category -m
php artisan make:model Tag -m
php artisan make:model Post -m
php artisan make:model Comment -m

# Create pivot table migration
php artisan make:migration create_post_tag_table

Categories Migration

database/migrations/create_categories_table.php
<?php
public function up(): void
{
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('slug')->unique();
        $table->text('description')->nullable();
        $table->string('color', 7)->default('#6366f1'); // hex color
        $table->timestamps();
    });
}

Tags Migration

database/migrations/create_tags_table.php
<?php
public function up(): void
{
    Schema::create('tags', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('slug')->unique();
        $table->timestamps();
    });
}

Posts Migration

database/migrations/create_posts_table.php
<?php
public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')
              ->constrained()
              ->onDelete('cascade');
        $table->foreignId('category_id')
              ->nullable()
              ->constrained()
              ->onDelete('set null');
        $table->string('title');
        $table->string('slug')->unique();
        $table->text('excerpt')->nullable();
        $table->longText('body');
        $table->string('image')->nullable();
        $table->boolean('published')->default(false);
        $table->timestamp('published_at')->nullable();
        $table->unsignedInteger('views')->default(0);
        $table->timestamps();
        $table->softDeletes(); // deleted_at column

        // Index for faster queries
        $table->index(['published', 'published_at']);
        $table->index('slug');
    });
}

Post-Tag Pivot Migration

database/migrations/create_post_tag_table.php
<?php
public function up(): void
{
    Schema::create('post_tag', function (Blueprint $table) {
        $table->foreignId('post_id')
              ->constrained()
              ->onDelete('cascade');
        $table->foreignId('tag_id')
              ->constrained()
              ->onDelete('cascade');
        $table->primary(['post_id', 'tag_id']);
    });
}

Comments Migration

database/migrations/create_comments_table.php
<?php
public function up(): void
{
    Schema::create('comments', function (Blueprint $table) {
        $table->id();
        $table->foreignId('post_id')
              ->constrained()
              ->onDelete('cascade');
        $table->foreignId('user_id')
              ->nullable()
              ->constrained()
              ->onDelete('set null');
        $table->string('author_name')->nullable(); // for guest comments
        $table->string('author_email')->nullable();
        $table->text('body');
        $table->boolean('approved')->default(false);
        $table->timestamps();
    });
}

Run Migrations

Terminal
php artisan migrate

# Check what was created
php artisan migrate:status

Add Database Seeder

database/seeders/DatabaseSeeder.php
<?php
public function run(): void
{
    // Create admin user
    User::factory()->create([
        'name'  => 'Admin',
        'email' => 'admin@blog.com',
    ]);

    // Create categories
    $categories = ['Laravel', 'PHP', 'JavaScript', 'DevOps', 'Tutorial'];
    foreach ($categories as $cat) {
        Category::create([
            'name' => $cat,
            'slug' => Str::slug($cat),
        ]);
    }

    // Create tags
    $tags = ['beginner', 'advanced', 'tips', 'howto', 'guide'];
    foreach ($tags as $tag) {
        Tag::create(['name' => $tag, 'slug' => $tag]);
    }

    // Create sample posts
    Post::factory(20)->create();
}

# Run seeder
# php artisan db:seed
💡
Always add indexes! The index() calls on frequently searched columns (slug, published) make queries much faster as your blog grows to thousands of posts.
💡
SoftDeletes is important! With softDeletes(), deleted posts aren't permanently removed — they just get a deleted_at timestamp. You can restore them later if needed.
← Previous Next Lesson →
💡

Stuck? Need help?

Review the previous lessons or check the Laravel documentation.

Laravel Docs →