To build an event calendar in Laravel 13, create an events table and model, add a JSON endpoint that returns events between a start and end date, and load FullCalendar on a Blade page with its events option pointing at that endpoint. FullCalendar sends the visible date range on every navigation, so your server only returns what is needed.
This guide uses Laravel 13 (PHP 8.3 or newer). The same code works on Laravel 12. FullCalendar is a separate JavaScript library, so check its documentation for the current version and installation options.
How does the calendar get its data?
FullCalendar can load events from a URL. When the user changes month or view, it requests that URL with start and end query parameters in ISO 8601 format. Your controller filters events that overlap this range and returns JSON objects with at least title and start.
Step 1: How do I create the model and table?
php artisan make:model Event -mSchema::create('events', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->dateTime('starts_at');
$table->dateTime('ends_at');
$table->timestamps();
$table->index(['starts_at', 'ends_at']);
});The index speeds up the range query as the table grows. Then set fillable fields and casts on the model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
protected $fillable = ['title', 'starts_at', 'ends_at'];
protected function casts(): array
{
return [
'starts_at' => 'datetime',
'ends_at' => 'datetime',
];
}
}Run php artisan migrate to create the table.
Step 2: How do I build the JSON feed?
namespace App\Http\Controllers;
use App\Models\Event;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CalendarFeedController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$data = $request->validate([
'start' => ['required', 'date'],
'end' => ['required', 'date', 'after_or_equal:start'],
]);
$events = Event::query()
->where('starts_at', '<', $data['end'])
->where('ends_at', '>=', $data['start'])
->get(['id', 'title', 'starts_at', 'ends_at'])
->map(fn (Event $e) => [
'id' => $e->id,
'title' => $e->title,
'start' => $e->starts_at->toIso8601String(),
'end' => $e->ends_at->toIso8601String(),
]);
return response()->json($events);
}
}The two where clauses return any event that overlaps the range, including events that start before it and end inside it. I ran php -l on this controller. I did not execute it against a database or a browser for this article.
Add the routes:
use App\Http\Controllers\CalendarFeedController;
Route::view('/calendar', 'calendar')->middleware('auth');
Route::get('/calendar/feed', CalendarFeedController::class)->middleware('auth');Step 3: How do I show the calendar in Blade?
Create resources/views/calendar.blade.php. Load FullCalendar from a CDN or from npm, following its documentation, then initialise it:
<div id="calendar"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const calendar = new FullCalendar.Calendar(document.getElementById('calendar'), {
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,listWeek'
},
events: '/calendar/feed'
});
calendar.render();
});
</script>How do I let users add events?
Keep it simple first: a normal form that posts to a store route with validation, then redirects back to the calendar.
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'title' => ['required', 'string', 'max:120'],
'starts_at' => ['required', 'date'],
'ends_at' => ['required', 'date', 'after:starts_at'],
]);
Event::create($data);
return redirect('/calendar');
}FullCalendar also supports clicking a date and editing events, but those features need extra JavaScript and more routes for update and delete. Add them only when you need them, and protect each route with authorization.
What about time zones?
Store datetimes in UTC and convert for display. Laravel's app.timezone setting controls the default. FullCalendar has its own timeZone option, so make sure the feed and the calendar agree. A mismatch makes events appear hours off.
How do I verify it works?
- Insert two events with
php artisan tinker: one this month, one next month. - Open
/calendar. Only the current month's event should show. - Click next. The next month's event appears, and the network tab shows a new request to
/calendar/feedwith differentstartandendvalues. - Request the feed without parameters. You should get a 422 validation response.
What are the common mistakes?
- Returning every event. It works at first and slows down as data grows. Filter by the range.
- Wrong keys in JSON. FullCalendar expects
start, notstarts_at. - Time zone drift. Keep one convention for storage and display.
- Missing auth on the feed. If events are private, the JSON route must be protected too.
- Using a reserved class name. Naming a model
Eventcan clash with imports of theEventfacade. Use an alias or a more specific name such asCalendarEventin larger apps.
FAQ
Do I need a package for a calendar in Laravel?
No. A model, a JSON route and a JavaScript calendar library such as FullCalendar are enough for most cases.
Can events repeat weekly?
FullCalendar supports recurring events through its rules, or you can generate occurrences on the server for the requested range. Start with single events and add recurrence later.
Can I use Livewire or Inertia instead?
Yes. The server side stays the same: a validated range query returning event data. Only the front-end wiring changes.



