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
User Roles
- Admin: Manage books, members, and view reports.
- Librarian: Issue/return books, handle fines.
- Member: Browse catalog, request loans, view history.
Book Management
- CRUD operations (Create, Read, Update, Delete).
- Categories & authors.
- Stock tracking (total copies, available copies).
Member Management
- Registration & profile management.
- Membership approval by admin.
Loan Processing
- Issue books with due dates.
- Return books and calculate fines for late returns.
Search & Filters
- Search by title, author, ISBN, category.
- Filter by availability, category.
Notifications
- Email reminders for due/overdue books.
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
Create a new Laravel app
composer create-project laravel/laravel library-system "12.*" cd library-systemServe locally
php artisan serve # Visit http://127.0.0.1:8000Install 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
- Run
php artisan migrate - 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:
Generate the middleware using Artisan:
php artisan make:middleware CheckRoleThis creates the file
app/Http/Middleware/CheckRole.php, where you can implement your role‑checking logic.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
roleproperty 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 aMiddlewareregistrar 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 passesadminas the$roleparameter to yourCheckRolemiddleware. - You can combine this alias with any other middleware defined in your application or Laravel’s built‑in aliases like
authandverified
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
- Mail Setup: Configure SMTP in
.env. - Mailable:
php artisan make:mail DueReminderMail - Scheduled Command:
In the command’sphp artisan make:command SendDueRemindershandle():Loan::whereDate('due_at', now()->addDay()) ->whereNull('returned_at') ->get() ->each(function($loan) { Mail::to($loan->member->user->email) ->send(new DueReminderMail($loan)); }); - 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
- Prepare:
php artisan config:cache php artisan route:cache php artisan view:cache - Server: Use Forge, Vapor, or shared host (point
public/directory). - .env: Set production credentials, mail, and queue drivers.
- Queues: Run
supervisorfor background jobs (email reminders). - 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!
Projects to explore next
Source-code projects from the KritiMyantra catalogue. Check each project's details before buying.
Flask Authentication System (Free Version) - Kritim Yantra
🛠 Features✔ Google & GitHub Authentication✔ User Registration & Login✔ Profile/Dashboard Access✔ Secure & Mi...
Flask Blog CMS with Admin Dashboard — Python Source Code
A Flask-based blogging website with a built-in admin dashboard. Manage articles, images, categories, tags and featured p...
Laravel 12 + ReactJS Starter Kit: Add Multi-Language Support (Arabic, English, Spanish) with Blog CRUD
🚀 Installation & Setup Download & Extract Get the ZIP from kritimyantra.com and extract it. Includes preconfigu...
Comments
No comments yet. Be the first to comment.
Sign in to join the discussion.