Building a Library Management System in Laravel 12: A Step-by-Step Guide

Author

Kritim Yantra

Apr 19, 2025

Building a Library Management System in Laravel 12: A Step-by-Step Guide

Introduction

Building a Library Management System is a classic project that lets you apply core Laravel concepts—from routing and MVC structure to database design, authentication, and more. In this guide, we’ll walk through creating a full-featured Library Management System using Laravel 12, covering everything from initial setup to deployment. Whether you’re a beginner or looking to sharpen your Laravel skills, you’ll find clear, step‑by‑step instructions and best practices throughout.


Features Overview

  1. User Roles

    • Admin: Manage books, members, and view reports.
    • Librarian: Issue/return books, handle fines.
    • Member: Browse catalog, request loans, view history.
  2. Book Management

    • CRUD operations (Create, Read, Update, Delete).
    • Categories & authors.
    • Stock tracking (total copies, available copies).
  3. Member Management

    • Registration & profile management.
    • Membership approval by admin.
  4. Loan Processing

    • Issue books with due dates.
    • Return books and calculate fines for late returns.
  5. Search & Filters

    • Search by title, author, ISBN, category.
    • Filter by availability, category.
  6. Notifications

    • Email reminders for due/overdue books.
  7. Reporting

    • Dashboard with summary (total books, active loans, overdue items).

Prerequisites

Before you begin, ensure you have:

  • PHP 8.2+ and Composer
  • Node.js & npm (for compiling assets)
  • Laravel Installer (optional)
  • Familiarity with MVC, routing, Blade templates, and basic Eloquent ORM.

1. Setting Up the Project

  1. Create a new Laravel app

    composer create-project laravel/laravel library-system "12.*"
    cd library-system
    
  2. Serve locally

    php artisan serve
    # Visit http://127.0.0.1:8000
    
  3. Install front‑end dependencies

    npm install
    npm run dev
    

2. Database Design & Migrations

2.1. Schema Overview

  • books: id, title, isbn, author_id, category_id, total_copies, available_copies, ...
  • authors: id, name, bio
  • categories: id, name
  • members: id, user_id, membership_date, status
  • loans: id, book_id, member_id, issued_at, due_at, returned_at, fine_amount

2.2. Create Migrations

php artisan make:migration create_authors_table
php artisan make:migration create_categories_table
php artisan make:migration create_books_table
php artisan make:migration create_members_table
php artisan make:migration create_loans_table

Edit each migration under database/migrations/:

2025_04_19_000000_create_authors_table.php

<?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('authors', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('bio')->nullable();
            $table->timestamps();
        });
    }

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

2025_04_19_000001_create_categories_table.php

<?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('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });
    }

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

2025_04_19_000002_create_books_table.php

<?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('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('isbn')->unique();
            $table->foreignId('author_id')->constrained()->onDelete('cascade');
            $table->foreignId('category_id')->constrained()->onDelete('cascade');
            $table->unsignedInteger('total_copies');
            $table->unsignedInteger('available_copies');
            $table->timestamps();
        });
    }

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

2025_04_19_000003_create_members_table.php

<?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('members', function (Blueprint $table) {
            $table->id();
            // Assumes you have a users table already for authentication
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->date('membership_date')->default(DB::raw('CURRENT_DATE'));
            $table->string('status')->default('pending'); // e.g. pending, active, suspended
            $table->timestamps();
        });
    }

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

2025_04_19_000004_create_loans_table.php

<?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('loans', function (Blueprint $table) {
            $table->id();
            $table->foreignId('book_id')->constrained()->onDelete('cascade');
            $table->foreignId('member_id')->constrained()->onDelete('cascade');
            $table->timestamp('issued_at')->useCurrent();
            $table->timestamp('due_at');
            $table->timestamp('returned_at')->nullable();
            $table->decimal('fine_amount', 8, 2)->default(0);
            $table->timestamps();
        });
    }

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

Next Steps

  1. Run
    php artisan migrate
    
  2. Verify your tables in the database.

3. Eloquent Models & Relationships

Create models with their relationships:

php artisan make:model Author -m
php artisan make:model Category -m
php artisan make:model Book -m
php artisan make:model Member -m
php artisan make:model Loan -m

Define relationships:

// app/Models/Author.php
public function books()
{
    return $this->hasMany(Book::class);
}

// app/Models/Category.php
public function books()
{
    return $this->hasMany(Book::class);
}

// app/Models/Book.php
public function author()
{
    return $this->belongsTo(Author::class);
}
public function category()
{
    return $this->belongsTo(Category::class);
}
public function loans()
{
    return $this->hasMany(Loan::class);
}

// app/Models/Member.php
public function user()
{
    return $this->belongsTo(User::class);
}
public function loans()
{
    return $this->hasMany(Loan::class);
}

// app/Models/Loan.php
public function book()
{
    return $this->belongsTo(Book::class);
}
public function member()
{
    return $this->belongsTo(Member::class);
}

4. Authentication & Roles

Use Laravel Breeze or Jetstream for authentication:

composer require laravel/breeze --dev
php artisan breeze:install
npm install && npm run dev
php artisan migrate

Add a role field to users table via a migration:

php artisan make:migration add_role_to_users_table
// in migration
$table->string('role')->default('member');

Seed an admin user:

User::factory()->create([
  'name' => 'Admin User',
  'email' => 'admin@example.com',
  'password' => bcrypt('secret'),
  'role' => 'admin',
]);

Protect routes by middleware:

  1. Generate the middleware using Artisan:

    php artisan make:middleware CheckRole
    

    This creates the file app/Http/Middleware/CheckRole.php, where you can implement your role‑checking logic.

  2. In app/Http/Middleware/CheckRole.php, add your authorization logic. For example:

    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    
    class CheckRole
    {
        public function handle(Request $request, Closure $next, string $role)
        {
            if (! $request->user() || $request->user()->role !== $role) {
                abort(403, 'Unauthorized.');
            }
            return $next($request);
        }
    }
    

    This middleware checks the authenticated user’s role property against the required role parameter.


2. Register the Middleware Alias in bootstrap/app.php

Starting in Laravel 12, you no longer register middleware aliases in the HTTP kernel. Instead, use the withMiddleware() callback in bootstrap/app.php:

<?php

use App\Http\Middleware\CheckRole;
use Illuminate\Foundation\Application;
use Illuminate\Routing\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        console: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Define a short alias 'role' for the CheckRole middleware
        $middleware->alias([
            'role' => CheckRole::class,
        ]);
    })
    ->withExceptions(function ($exceptions) {
        // Exception handling...
    })
    ->create();
  • The withMiddleware() method accepts a closure that receives a Middleware registrar instance.
  • Inside this closure, call $middleware->alias() to map the 'role' key to your middleware class.
  • You can define multiple aliases at once:
    $middleware->alias([
        'role'       => CheckRole::class,
        'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
    ]);
    

3. Applying the Middleware Alias to Routes

With the alias registered, you can attach it to individual routes or entire route groups without referencing the class name:

// routes/web.php

// Protect a single route with the 'role:admin' middleware
Route::get('/admin/dashboard', [AdminController::class, 'index'])
     ->middleware('role:admin');

// Apply to a group of routes
Route::middleware(['auth', 'role:librarian'])->group(function () {
    Route::get('/librarian/books', [BookController::class, 'index']);
    // ... other librarian routes
});
  • The 'role:admin' syntax passes admin as the $role parameter to your CheckRole middleware.
  • You can combine this alias with any other middleware defined in your application or Laravel’s built‑in aliases like auth and verified

4. (Optional) Adjusting Middleware Priority or Groups

Laravel 12 also lets you reorder middleware execution or modify predefined groups in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    // Ensure CheckRole runs after Authenticate
    $middleware->priority([
        \Illuminate\Auth\Middleware\Authenticate::class,
        CheckRole::class,
    ]);

    // Add CheckRole to the 'web' group
    $middleware->web(append: [
        CheckRole::class,
    ]);
})
  • Use $middleware->priority() to specify execution order.
  • Use $middleware->web() or $middleware->api() to append or prepend middleware to the default groups.

This ensures your role alias correctly guards routes based on the user’s role.


5. CRUD Controllers & Routes

Generate resource controllers:

php artisan make:controller Admin/AuthorController --resource --model=Author
php artisan make:controller Admin/CategoryController --resource --model=Category
php artisan make:controller Admin/BookController --resource --model=Book
php artisan make:controller Member/LoanController --resource --model=Loan

Define routes in routes/web.php:

Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
    Route::resource('authors', AuthorController::class);
    Route::resource('categories', CategoryController::class);
    Route::resource('books', BookController::class);
});

Route::middleware(['auth', 'role:member'])->group(function () {
    Route::resource('loans', LoanController::class);
});

Implement CRUD methods (index, create, store, show, edit, update, destroy) in each controller, using standard validation:

// In BookController@store
$request->validate([
    'title' => 'required|string|max:255',
    'isbn'  => 'required|string|unique:books',
    'author_id'   => 'required|exists:authors,id',
    'category_id' => 'required|exists:categories,id',
    'total_copies' => 'required|integer|min:1',
]);

Book::create([
    ...$request->only(['title','isbn','author_id','category_id']),
    'available_copies' => $request->total_copies,
]);

6. Loan Processing Logic

In LoanController:

public function store(Request $request)
{
    $request->validate([
        'book_id' => 'required|exists:books,id',
    ]);

    $book = Book::findOrFail($request->book_id);
    if ($book->available_copies < 1) {
        return back()->withErrors('No copies available');
    }

    $loan = Loan::create([
        'book_id'   => $book->id,
        'member_id' => auth()->user()->member->id,
        'issued_at' => now(),
        'due_at'    => now()->addWeeks(2),
    ]);

    $book->decrement('available_copies');
    return redirect()->route('loans.index')->with('success', 'Book issued successfully.');
}

public function update(Request $request, Loan $loan)
{
    $loan->update([
        'returned_at' => now(),
        'fine_amount' => $loan->due_at->isPast()
            ? now()->diffInDays($loan->due_at) * 5
            : 0,
    ]);
    $loan->book->increment('available_copies');
    return back()->with('success', 'Book returned.');
}

7. Blade Views & UI

  • Layouts: resources/views/layouts/app.blade.php.
  • Partials: Navigation, flash messages, form error display.
  • Resource Views:
    • authors/index.blade.php, create.blade.php, edit.blade.php.
    • Similarly for categories, books, loans.

Use Bootstrap or TailwindCSS (built into Breeze) for styling:

@if(session('success'))
  <div class="alert alert-success">{{ session('success') }}</div>
@endif

8. Search & Filtering

Add search form in BookController@index:

public function index(Request $request)
{
    $query = Book::with(['author','category']);
    if ($search = $request->input('q')) {
        $query->where('title', 'like', "%{$search}%")
              ->orWhere('isbn', 'like', "%{$search}%");
    }
    $books = $query->paginate(10);
    return view('admin.books.index', compact('books'));
}

In Blade:

<form method="GET" action="{{ route('admin.books.index') }}">
  <input type="text" name="q" value="{{ request('q') }}" placeholder="Search books…">
  <button type="submit">Search</button>
</form>

9. Notifications & Email Reminders

  1. Mail Setup: Configure SMTP in .env.
  2. Mailable:
    php artisan make:mail DueReminderMail
    
  3. Scheduled Command:
    php artisan make:command SendDueReminders
    
    In the command’s handle():
    Loan::whereDate('due_at', now()->addDay())
        ->whereNull('returned_at')
        ->get()
        ->each(function($loan) {
            Mail::to($loan->member->user->email)
                ->send(new DueReminderMail($loan));
        });
    
  4. Schedule in app/Console/Kernel.php:
    $schedule->command('reminders:due')->dailyAt('08:00');
    

10. Testing

  • Unit Tests for Models (relationships, accessors).
  • Feature Tests for HTTP routes, form submissions.
  • Example with PHPUnit:
public function test_admin_can_create_book()
{
    $admin = User::factory()->create(['role' => 'admin']);
    $this->actingAs($admin)
         ->post(route('admin.books.store'), [
             'title' => 'Test Book',
             'isbn'  => '1234567890',
             'author_id' => Author::factory()->create()->id,
             'category_id' => Category::factory()->create()->id,
             'total_copies' => 5,
         ])
         ->assertRedirect(route('admin.books.index'));

    $this->assertDatabaseHas('books', ['title' => 'Test Book']);
}

Run tests:

php artisan test

11. Deployment

  1. Prepare:
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    
  2. Server: Use Forge, Vapor, or shared host (point public/ directory).
  3. .env: Set production credentials, mail, and queue drivers.
  4. Queues: Run supervisor for background jobs (email reminders).
  5. SSL: Obtain certificates via Let’s Encrypt.

Conclusion & Next Steps

You’ve built a robust Library Management System in Laravel 12, covering:

  • Environment setup and migrations
  • Models, controllers, and relationships
  • Authentication, authorization, and user roles
  • Book & member CRUD
  • Loan issuance, returns, and fines
  • Search, filtering, and email notifications
  • Testing and deployment best practices

To deepen your knowledge:

  • Integrate API endpoints with Laravel Sanctum.
  • Add real‑time updates via broadcasting (WebSockets).
  • Implement fine reporting charts using a JS library.
  • Explore cache optimization for large catalogs.

Keep iterating on the project—add new features, refactor code, and follow the latest Laravel releases. Happy coding!

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts