Kritim Yantra
Apr 19, 2025
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.
User Roles
Book Management
Member Management
Loan Processing
Search & Filters
Notifications
Reporting
Before you begin, ensure you have:
Create a new Laravel app
composer create-project laravel/laravel library-system "12.*"
cd library-system
Serve locally
php artisan serve
# Visit http://127.0.0.1:8000
Install front‑end dependencies
npm install
npm run dev
id, title, isbn, author_id, category_id, total_copies, available_copies, ...
id, name, bio
id, name
id, user_id, membership_date, status
id, book_id, member_id, issued_at, due_at, returned_at, fine_amount
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');
}
};
php artisan migrate
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);
}
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 CheckRole
This 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 role
property against the required role parameter.
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();
withMiddleware()
method accepts a closure that receives a Middleware
registrar instance. $middleware->alias()
to map the 'role'
key to your middleware class. $middleware->alias([
'role' => CheckRole::class,
'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
]);
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
});
'role:admin'
syntax passes admin
as the $role
parameter to your CheckRole
middleware. auth
and verified
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,
]);
})
$middleware->priority()
to specify execution order. $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.
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,
]);
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.');
}
resources/views/layouts/app.blade.php
. authors/index.blade.php
, create.blade.php
, edit.blade.php
. Use Bootstrap or TailwindCSS (built into Breeze) for styling:
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
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>
.env
. php artisan make:mail DueReminderMail
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));
});
app/Console/Kernel.php
: $schedule->command('reminders:due')->dailyAt('08:00');
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
php artisan config:cache
php artisan route:cache
php artisan view:cache
public/
directory). supervisor
for background jobs (email reminders). You’ve built a robust Library Management System in Laravel 12, covering:
To deepen your knowledge:
Keep iterating on the project—add new features, refactor code, and follow the latest Laravel releases. Happy coding!
Transform from beginner to Laravel expert with our personalized Coaching Class starting June 21, 2025. Limited enrollment ensures focused attention.
1-hour personalized coaching
Build portfolio applications
Industry-standard techniques
Interview prep & job guidance
Complete your application to secure your spot
Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google