If you’re just stepping into Laravel API development, building a simple Event Management API is a great way to understand the fundamentals. In this guide, I’ll walk you through each step you listed, explain why we do it, and show small examples so things stay clear and beginner-friendly.
1. Create a New Laravel App
First, create a fresh Laravel project:
composer create-project laravel/laravel event-management
cd event-management
php artisan serveNow your app is running on:
http://127.0.0.1:80002. Install Helpful Development Packages
🔹 Debugbar
composer require fruitcake/laravel-debugbar --devWhy?
This package shows useful debug info (queries, request data, performance) in your app. It’s mostly helpful during development.
🔹 Artisan Browse
composer require joshembling/artisan-browseWhy?
It lets you quickly inspect your database tables directly from the terminal.
Example:
php artisan browse3. Database Setup, Migration & Seeding
Configure .env
Set your database credentials:
DB_DATABASE=event_management
DB_USERNAME=root
DB_PASSWORD=Run Default Migrations
php artisan migrateThis creates default tables like users, password_reset_tokens, etc.
Seed the Database
Laravel includes a default seeder for users.
php artisan db:seedNow your users table will have sample data.
4. API-Only Approach (No Views)
You mentioned:
“we don’t need browser form/layout/access”
That means:
- ❌ No Blade templates
- ❌ No form submissions
- ✅ Only JSON requests/responses
Example response:
{
"message": "Event created successfully"
}5. Create Models & Migrations
Create Event Model
php artisan make:model Event -mCreate Attendee Model
php artisan make:model Attendee -mThis generates:
- Model file
- Migration file
6. Define Database Structure
Event Migration Example
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});Attendee Migration Example
Schema::create('attendees', function (Blueprint $table) {
$table->id();
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});Then run:
php artisan migrate7. Create API Controllers
php artisan make:controller Api/EventController --api
php artisan make:controller Api/AttendeeController --apiWhy --api?
It creates a controller without:
create()edit()
Because those are for HTML forms, which we are not using.
Example Controller Methods
public function index()
{
return Event::all();
}
public function store(Request $request)
{
$event = Event::create($request->all());
return response()->json($event, 201);
}8. Define Relationships
Event Model
class Event extends Model
{
protected $fillable = ['title', 'description', 'user_id'];
public function owner()
{
return $this->belongsTo(User::class, 'user_id');
}
public function attendees()
{
return $this->hasMany(Attendee::class);
}
}Attendee Model
class Attendee extends Model
{
protected $fillable = ['event_id', 'user_id'];
public function event()
{
return $this->belongsTo(Event::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}User Model (update)
public function events()
{
return $this->hasMany(Event::class);
}
public function attendingEvents()
{
return $this->belongsToMany(Event::class, 'attendees');
}9. Relationship Summary
- ✅ One User owns many Events
- ✅ One Event has many Attendees
- ✅ One User can attend many Events
This structure is very common in real-world apps.
10. Define API Routes (Nested Resources)
Instead of simple routes, you’re using nested API resources, which is a smarter and more realistic approach for related data like events and attendees.
Open routes/api.php and add:
use App\Http\Controllers\Api\EventController;
use App\Http\Controllers\Api\AttendeeController;
Route::apiResource('events', EventController::class);
Route::apiResource('events.attendees', AttendeeController::class)
->scoped(['attendee' => 'event']);
🔍 What’s Different Here?
1. Standard Event Routes
Route::apiResource('events', EventController::class);This gives you endpoints like:
GET /api/eventsPOST /api/eventsGET /api/events/{event}
2. Nested Attendee Routes
Route::apiResource('events.attendees', AttendeeController::class);Now attendees are always accessed within an event context:
| Method | Endpoint |
|---|---|
| GET | /api/events/{event}/attendees |
| POST | /api/events/{event}/attendees |
| GET | /api/events/{event}/attendees/{attendee} |
| PUT | /api/events/{event}/attendees/{attendee} |
| DELETE | /api/events/{event}/attendees/{attendee} |
👉 This makes your API more logical:
- You don’t fetch attendees globally
- You always know which event they belong to
3. Scoped Binding (Important 🚨)
->scoped(['attendee' => 'event']);This ensures:
- The
attendeemust belong to the givenevent - Prevents accessing unrelated data
🧠 Example
Without scoped binding:
GET /api/events/1/attendees/99Even if attendee 99 belongs to another event, Laravel might still return it ❌
With scoped binding:
- Laravel checks:”Does attendee 99 belong to event 1?”
If not:
{
"message": "Not Found"
}✅ This adds data safety automatically
🎯 Why This Approach Is Better
- Cleaner API structure
- Strong relationship enforcement
- Prevents invalid data access
- Matches real-world logic
⚡ Controller Tip
In your AttendeeController, you can now safely assume:
public function index(Event $event)
{
return $event->attendees;
}Laravel automatically injects the correct Event model based on the URL.
This small routing change makes your API much more robust and professional 👍
11. Testing Your API
You can use:
- Postman
- Thunder Client (VS Code)
- curl
Example request:
POST /api/events{
"title": "Laravel Workshop",
"description": "Learning APIs",
"user_id": 1
}🎯 Final Thoughts
This setup gives you:
- A clean API-only Laravel project
- Proper database relationships
- Scalable structure for real apps
From here, you can extend:
- Authentication (login/register)
- Validation
- API Resources (for better JSON formatting)
- Pagination
