API Integration
API Integration Guide
Learn how to connect your FullEventCalendar to external data sources and APIs to manage event data.
Overview
FullEventCalendar is designed to work seamlessly with any API or data source. The component provides several handler props that you can use to integrate with your backend:
onEventAdd
- Called when a new event is createdonEventUpdate
- Called when an existing event is updatedonEventDelete
- Called when an event is deletedonDateRangeChange
- Called when the visible date range changes
Each of these handlers is Promise-based, allowing you to make asynchronous API calls to your backend.
Basic Integration Pattern
Here's the basic pattern for integrating with an API:
import { EventCalendar } from '@/components/event-calendar';
import { useEventCalendar } from '@/components/event-calendar/hooks/useEventCalendar';
import { CalendarEventType } from '@/components/event-calendar/types';
const CalendarWithAPI = () => {
const {
events,
isLoading,
addEvent,
updateEvent,
deleteEvent,
onViewOrDateChange
} = useEventCalendar({
initialEvents: [],
onEventAdd: async (event) => {
// API call to create event
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to create event');
}
// Return the created event with an ID
return await response.json();
},
onEventUpdate: async (event) => {
// API call to update event
const response = await fetch(`/api/events/${event.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to update event');
}
return event;
},
onEventDelete: async (eventId) => {
// API call to delete event
const response = await fetch(`/api/events/${eventId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete event');
}
},
onDateRangeChange: async (startDate, endDate, signal) => {
// API call to fetch events for date range
const start = startDate.toISOString();
const end = endDate.toISOString();
const response = await fetch(
`/api/events?start=${start}&end=${end}`,
{ signal }
);
if (!response.ok) {
throw new Error('Failed to fetch events');
}
return await response.json();
}
});
return (
<EventCalendar
events={events}
isLoading={isLoading}
onEventAdd={addEvent}
onEventUpdate={updateEvent}
onEventDelete={deleteEvent}
onDateRangeChange={onViewOrDateChange}
/>
);
};
Error Handling
The useEventCalendar
hook includes built-in error handling. When an error occurs:
- The operation fails gracefully
- The calendar remains in a consistent state
- An error toast notification is shown to the user
You can also implement your own error handling:
const {
events,
isLoading,
addEvent,
updateEvent,
deleteEvent,
onViewOrDateChange
} = useEventCalendar({
// ...other props
onEventAdd: async (event) => {
try {
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
// You can throw a custom error with a user-friendly message
throw new Error('Unable to save event. Please try again later.');
}
return await response.json();
} catch (error) {
// Log error for debugging
console.error('Create event error:', error);
// Rethrow the error for the hook to handle
throw error;
}
},
// Similar error handling for other operations...
});
Loading States
The calendar can show loading states when fetching events. The isLoading
state is automatically managed by the useEventCalendar
hook:
<EventCalendar
events={events}
isLoading={isLoading} // Automatically managed by the hook
// ...other props
/>
When isLoading
is true, the calendar displays skeleton loaders in place of events.
Pagination and Lazy Loading
For large datasets, you can implement pagination or lazy loading using the onDateRangeChange
handler:
onDateRangeChange: async (startDate, endDate, signal) => {
// Convert dates to ISO strings for API parameters
const start = startDate.toISOString();
const end = endDate.toISOString();
// Optional: Add pagination parameters
const limit = 100;
const page = 1;
const response = await fetch(
`/api/events?start=${start}&end=${end}&limit=${limit}&page=${page}`,
{ signal }
);
if (!response.ok) {
throw new Error('Failed to fetch events');
}
return await response.json();
}
The signal
parameter is an AbortSignal that you can pass to your fetch request. This allows the calendar to cancel in-flight requests when the user navigates quickly between date ranges.
Real-time Updates
To support real-time updates (e.g., from WebSockets), you can update the events array directly:
import { useState, useEffect } from 'react';
import { EventCalendar } from '@/components/event-calendar';
import { CalendarEventType } from '@/components/event-calendar/types';
const RealtimeCalendar = () => {
const [events, setEvents] = useState<CalendarEventType[]>([]);
useEffect(() => {
// Set up WebSocket connection
const socket = new WebSocket('wss://your-api.com/events');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle different types of updates
if (data.type === 'new_event') {
setEvents(prevEvents => [...prevEvents, data.event]);
} else if (data.type === 'update_event') {
setEvents(prevEvents =>
prevEvents.map(event =>
event.id === data.event.id ? data.event : event
)
);
} else if (data.type === 'delete_event') {
setEvents(prevEvents =>
prevEvents.filter(event => event.id !== data.eventId)
);
}
};
return () => {
socket.close();
};
}, []);
// CRUD handlers that update both local state and backend
const handleAddEvent = async (event: Omit<CalendarEventType, 'id'>) => {
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) {
throw new Error('Failed to create event');
}
const newEvent = await response.json();
// No need to update state here if WebSocket will push the update
return newEvent;
};
// Similar handlers for update and delete...
return (
<EventCalendar
events={events}
onEventAdd={handleAddEvent}
// ...other handlers
/>
);
};
Authentication and Authorization
When working with authenticated APIs:
onEventAdd: async (event) => {
// Get authentication token (e.g., from cookie, localStorage, or state)
const token = getAuthToken();
const response = await fetch('/api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(event)
});
if (response.status === 401) {
// Handle authentication errors
redirectToLogin();
throw new Error('Please log in to continue');
}
if (!response.ok) {
throw new Error('Failed to create event');
}
return await response.json();
}
Custom Data Mapping
If your API uses a different data structure than what the calendar expects, you'll need to map between the two:
// Example API event structure
interface ApiEvent {
eventId: string;
title: string;
description: string;
beginAt: string; // ISO date string
finishAt: string; // ISO date string
category: string;
isRecurring: boolean;
recurrenceType?: string;
}
// Mapping functions
const mapToCalendarEvent = (apiEvent: ApiEvent): CalendarEventType => {
const startDate = new Date(apiEvent.beginAt);
const endDate = new Date(apiEvent.finishAt);
return {
id: apiEvent.eventId,
title: apiEvent.title,
description: apiEvent.description || undefined,
startDate: new Date(startDate.toDateString()),
endDate: new Date(endDate.toDateString()),
startTime: startDate.toTimeString().substring(0, 5),
endTime: endDate.toTimeString().substring(0, 5),
color: mapCategoryToColor(apiEvent.category),
isRepeating: apiEvent.isRecurring,
isRepeatingInterval: apiEvent.recurrenceType as any
};
};
const mapToApiEvent = (calendarEvent: CalendarEventType): Omit<ApiEvent, 'eventId'> => {
const startDateTime = new Date(`${calendarEvent.startDate.toDateString()} ${calendarEvent.startTime}`);
const endDateTime = new Date(`${calendarEvent.endDate.toDateString()} ${calendarEvent.endTime}`);
return {
title: calendarEvent.title,
description: calendarEvent.description || '',
beginAt: startDateTime.toISOString(),
finishAt: endDateTime.toISOString(),
category: mapColorToCategory(calendarEvent.color),
isRecurring: calendarEvent.isRepeating,
recurrenceType: calendarEvent.isRepeatingInterval
};
};
// Usage in API calls
onEventAdd: async (event) => {
const apiEvent = mapToApiEvent(event);
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(apiEvent)
});
if (!response.ok) {
throw new Error('Failed to create event');
}
const createdApiEvent = await response.json();
return mapToCalendarEvent(createdApiEvent);
}
Backend Implementation Examples
Next.js API Route
// /pages/api/events/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { prisma } from '@/lib/prisma';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'GET') {
const { start, end } = req.query;
if (!start || !end) {
return res.status(400).json({ error: 'Start and end dates are required' });
}
try {
const events = await prisma.event.findMany({
where: {
OR: [
// Events that start within the range
{
startTime: {
gte: new Date(start as string),
lte: new Date(end as string)
}
},
// Events that end within the range
{
endTime: {
gte: new Date(start as string),
lte: new Date(end as string)
}
},
// Events that span the entire range
{
AND: [
{ startTime: { lte: new Date(start as string) } },
{ endTime: { gte: new Date(end as string) } }
]
}
]
}
});
// Map database events to calendar events
const calendarEvents = events.map(dbEventToCalendarEvent);
return res.status(200).json(calendarEvents);
} catch (error) {
console.error('Error fetching events:', error);
return res.status(500).json({ error: 'Failed to fetch events' });
}
} else if (req.method === 'POST') {
try {
const eventData = req.body;
// Convert calendar event to database model
const dbEvent = calendarEventToDbEvent(eventData);
const createdEvent = await prisma.event.create({
data: dbEvent
});
// Convert back to calendar event format
const calendarEvent = dbEventToCalendarEvent(createdEvent);
return res.status(201).json(calendarEvent);
} catch (error) {
console.error('Error creating event:', error);
return res.status(500).json({ error: 'Failed to create event' });
}
} else {
return res.status(405).json({ error: 'Method not allowed' });
}
}
Express API
// events.js
const express = require('express');
const router = express.Router();
const Event = require('../models/Event');
// Get events within date range
router.get('/', async (req, res) => {
const { start, end } = req.query;
if (!start || !end) {
return res.status(400).json({ error: 'Start and end dates are required' });
}
try {
const events = await Event.find({
$or: [
// Events that start within the range
{
startTime: {
$gte: new Date(start),
$lte: new Date(end)
}
},
// Events that end within the range
{
endTime: {
$gte: new Date(start),
$lte: new Date(end)
}
},
// Events that span the entire range
{
$and: [
{ startTime: { $lte: new Date(start) } },
{ endTime: { $gte: new Date(end) } }
]
}
]
});
// Map database events to calendar events
const calendarEvents = events.map(dbEventToCalendarEvent);
res.json(calendarEvents);
} catch (error) {
console.error('Error fetching events:', error);
res.status(500).json({ error: 'Failed to fetch events' });
}
});
// Create new event
router.post('/', async (req, res) => {
try {
const eventData = req.body;
// Convert calendar event to database model
const dbEvent = calendarEventToDbEvent(eventData);
const createdEvent = await Event.create(dbEvent);
// Convert back to calendar event format
const calendarEvent = dbEventToCalendarEvent(createdEvent);
res.status(201).json(calendarEvent);
} catch (error) {
console.error('Error creating event:', error);
res.status(500).json({ error: 'Failed to create event' });
}
});
module.exports = router;
Testing API Integration
When testing your API integration:
- Test all CRUD operations (Create, Read, Update, Delete)
- Test with various date ranges
- Test error handling scenarios
- Test performance with large datasets
- Test cancellation of in-flight requests
Conclusion
FullEventCalendar's API integration capabilities make it easy to connect to any backend service. By implementing the appropriate handler functions and data mapping, you can create a seamless experience that keeps your calendar in sync with your backend data.