How to Build a Calendar App - A Comprehensive Guide

Authors
Published on

If you’re a developer tasked with integrating a calendar into your application, or an entrepreneur who is thinking of building a calendar app, or an app that has calendar integrations, this article is for you.

Let’s learn how to build a calendar app, tackle each decision step by step, choose the best technologies, and learn the challenges and best practices.

If you’d like to see the full implementation and the codebase, please head over to our Unified Calendar View Example app on GitHub. All the instructions on how to run the application locally can be found under the README file.

What is the scope of this article?

As mentioned in the intro, the goal of this article is to help you build a full-fledged calendar app, or simply integrate calendar providers into your existing application, where users can connect their calendars and manage them through your app.

This article is also helpful even if you want to integrate with calendar providers without providing a calendar interface. Examples would be a tasks app, a dating app, or even a feature that inserts an event on a user’s calendars.

You can feel free to take certain parts of the article and use them in your codebase, including technology choices, unified APIs, or parts of the code.

Please head over to the OneCal Unified Calendar API if you’re looking to integrate all calendar providers using a single API.

The overview of the architecture & tech stack

The platform we’ve chosen to illustrate this example is the web, as it’s easier to get it up and running, and there is plenty of community support for calendar libraries.

The programming language we’ll use is TypeScript, and the web framework is Next.js.

Here is an overview of the tech stack we’ll be using:

  • Frontend: Next.js (app router, TypeScript)
  • Backend: Next.js API routes + tRPC.
  • Database: PostgreSQL (using Prisma as ORM)
  • Calendar APIs: OneCal Unified Calendar API
  • Hosting: Provider agnostic, you can host it in Vercel, or wherever you’re comfortable.
  • Authentication: OAuth2 (Google, Microsoft) through better-auth

Below is the architectural diagram and how the tech stack ties together:

Calendar App Architecture Diagram

Definitions:

  • Client (1): The end user device. This illustration uses a mobile device, but it could be a desktop, laptop, etc (a device that supports web browsers such as Google Chrome). The client is responsible for rendering the Next.js frontend, handling user interactions, and communicating with the backend through secure HTTPS requests.
  • Web Server (2): The Web Server hosts and serves the user interface, a Next.js (App Router) web application. It delivers optimized HTML, CSS, and JavaScript to the client and provides server-side rendering (SSR) and incremental static regeneration (ISR) for fast performance and SEO benefits.
  • Nextjs API (3): The Next.js API serves as the backend layer, implemented using API routes and tRPC within the same Next.js application. It functions as the central hub connecting the frontend, the database, and external integrations like the OneCal Unified Calendar API. Unlike traditional REST endpoints, tRPC enables type-safe, end-to-end communication between the frontend and backend without needing to define a separate API schema. This means the client can call backend procedures directly, with full TypeScript type inference. This helps in improving development speed and reducing runtime errors.
  • PostgreSQL(4): The PostgreSQL database stores all persistent application data, including users, sessions, connected calendar accounts, etc. It is the system of record for all user-related data and synchronization states. With Prisma as the ORM layer, the schema is cleanly mapped to the database, making migrations and queries easy to manage.
  • OneCal Unified Calendar API (5): The OneCal Unified Calendar API is the API we're using to integrate with all calendar providers using a standardized API. OneCal Unified makes it easy for us to integrate with all calendar providers and not worry about coding separate implementations, dealing with different data interfaces, maintaining multiple integrations, or dealing with API changes. Our API communicates with the OneCal Unified Calendar API by sending the API key and which calendars we want to perform operations on (CRUD events, calendars, and more), regardless of the provider. In response, OneCal communicates with the calendar providers and then sends the response in a standardized format for all calendar providers.

Note that the Web Server (2) and the Next.js API (3) can be hosted in the same server (using Vercel, Docker, etc), but they’re split just for visually understanding that there is a UI server and an API server, even though they are both within the same Next.js codebase and are generally hosted within the same server.

Designing the data model

After explaining the architecture and the tech stack, it’s time to define our data model. We’ll be using Prisma as our preferred ORM.

This Prisma schema defines the data structure for a basic calendar application that supports user authentication, calendar account connections (Google, Microsoft), and event synchronization through a unified API.

The most important models are:

1. User

Represents an end user of the app. Each user can have multiple sessions, connected accounts (OAuth), and calendar accounts. Fields like email, name, and onboardingCompletedAt help track profile and onboarding status.

2. CalendarAccount

Represents a linked external calendar account (e.g., a Google or Microsoft account). It stores the provider, email, and status (active or expired). Each CalendarAccount belongs to one User and can contain multiple Calendar entries.

3. Calendar

Represents an individual calendar (like “Work,” “Personal,” or “Family”) within a linked account. It includes display fields such as name, color, timezone, and flags like isPrimary or isReadOnly. Each calendar is linked to both a User and to the CalendarAccount it originates from.

4. Account

Handles OAuth provider data (Google or Microsoft). It stores access and refresh tokens, token expiry times, and scope information, used for authentication and calendar synchronization.

5. Session

Tracks active login sessions for users. Contains fields like token, expiresAt, ipAddress, and userAgent to manage and secure active sessions.

6. Verification

Used for one-time verifications, such as email login magic links or passwordless authentication codes. It stores temporary identifiers and expiration times.

7. Enums

  • CalendarAccountProvider: Defines supported providers (GOOGLE, MICROSOFT).
  • CalendarAccountStatus: Tracks whether a connected account is ACTIVE or EXPIRED.

The Database ER diagram:

Database ER Diagram

Please open the schema.prisma file in our GitHub repository to see the full database schema, including types and connections.

Building the Backend

As mentioned, we’ll use Next.js API routes to build the API, as it’s quite handy having the API and the UI in the same codebase and hosted on the same server. This means that you can run the UI and API simultaneously.

We chose to use the Next.js API routes because it makes sense from a complexity standpoint, as we’re building a simple calendar app, where users log in, connect their calendars, and perform operations like creating events, updating events, deleting events, etc. If you’re building something more complex, you’re free to use Node.js, Nest.js, or any other framework for that matter. The DB entities should be the same, and the OneCal Unified Calendar API we’re using to interact with all calendar providers is HTTP-based, so you can call it from any programming language or framework you use.

Note that we’re not using the Next.js API routes as they are, we’re using tRPC to make our APIs type safe end-to-end.

Building the authentication

We’ll be using better-auth as our authentication framework. Better auth makes authentication so easy and painless. Please follow the Better Auth Guide on how to integrate Better Auth with Next.js, as the steps are almost identical to that guide. You can feel free to explore the auth file in our repo to learn more.

Setting up the OneCal Unified Calendar API to communicate with all calendar providers

The major pain point when building a calendar app or when integrating calendars into an existing application or product feature is dealing with provider-specific APIs. This has a time cost associated with it, as we’d have to learn each API separately, deal with the different data structures, requests, responses, and more. Additionally, we’d need to build a separate integration for each provider + maintain the integrations after finishing the development.

A great solution to this problem is using a Unified Calendar API that helps us integrate with all calendar providers using a standardized API. In this example, we’ll be using the OneCal Unified Calendar API.

To get started with OneCal Unified, follow these steps:

  1. The first step is to sign up for OneCal Unified and create a free account.
    OneCal Unified Signup Page
  2. After signing up, please enable the calendar providers you’d like to integrate. We recommend enabling Google Calendar and Outlook, so we understand the power behind using a unified calendar api product. Note that you don’t need to create a Google or Microsoft Client, as for sandboxing and development, you can use the OneCal Unified Google Client, allowing you to connect Outlook or Google Calendar accounts to your application.
    Enable Calendar Provider
  3. Create an API key and store it in the env ONECAL_UNIFIED_API_KEY
    Create an API Key Illustration

Building the OneCal Unified API Client

After setting up the OneCal Unified Calendar API and retrieving the API key, it’s time to build the API client that interacts with the OneCal Unified Calendar API:

import { env } from "@/env";
import type {
  EndUserAccount,
  PaginatedResponse,
  UnifiedCalendar,
  UnifiedEvent as UniversalEvent,
} from "@/server/lib/onecal-unified/types";
import ky from "ky";

export const onecalUnifiedApi = ky.create({
  prefixUrl: env.NEXT_PUBLIC_ONECAL_UNIFIED_URL,
  headers: {
    "x-api-key": env.ONECAL_UNIFIED_API_KEY,
  },
});

export async function getEndUserAccountById(id: string) {
  const response = await onecalUnifiedApi.get<EndUserAccount>(
    `endUserAccounts/${id}`,
  );
  return response.json();
}

export async function getCalendarsForEndUserAccount(endUserAccountId: string) {
  const response = await onecalUnifiedApi.get<
    PaginatedResponse<UnifiedCalendar>
  >(`calendars/${endUserAccountId}`);
  return response.json();
}

interface GetCalendarEventsParams {
  pageToken?: string;
  pageSize?: number;
  syncToken?: string;
  startDateTime?: string;
  endDateTime?: string;
  timeZone?: string;
  expandRecurrences?: boolean;
}

export async function getCalendarEvents(
  endUserAccountId: string,
  calendarId: string,
  params: GetCalendarEventsParams = {},
) {
  const queryParams = new URLSearchParams(params as Record<string, string>);

  const response = await onecalUnifiedApi.get<
    PaginatedResponse<UniversalEvent>
  >(`events/${endUserAccountId}/${calendarId}?${queryParams}`);
  return response.json();
}

export async function getCalendarEvent(
  endUserAccountId: string,
  calendarId: string,
  eventId: string,
) {
  const response = await onecalUnifiedApi.get<UniversalEvent>(
    `events/${endUserAccountId}/${calendarId}/${eventId}`,
  );
  return response.json();
}

export async function createCalendarEvent(
  endUserAccountId: string,
  calendarId: string,
  event: Partial<UniversalEvent>,
) {
  const response = await onecalUnifiedApi.post<UniversalEvent>(
    `events/${endUserAccountId}/${calendarId}`,
    {json: event},
  );
  return response.json();
}

export async function editCalendarEvent(
  endUserAccountId: string,
  calendarId: string,
  eventId: string,
  event: Partial<UniversalEvent>,
) {
  const response = await onecalUnifiedApi.put<UniversalEvent>(
    `events/${endUserAccountId}/${calendarId}/${eventId}`,
    {json: event},
  );
  return response.json();
}

export async function deleteCalendarEvent(
  endUserAccountId: string,
  calendarId: string,
  eventId: string,
) {
  await onecalUnifiedApi.delete(
    `events/${endUserAccountId}/${calendarId}/${eventId}`,
  );
}

You can find the client types and other relevant classes in this GitHub location.

The sequence diagram explains how the Example Calendar App interacts with the OneCal Unified Calendar API to provide a seamless integration with all calendar providers.

API communication sequence diagram

Creating the API routes

After setting up the OneCal Unified Calendar API client, we should be good to go to create the API routes for managing calendar accounts and calendar events. Note that we don't need to explicitly create session APIs as we're leveraging BetterAuth for this purpose.

The API contains route definitions for:

  • Calendar accounts: Route that exposes HTTP methods to list all calendar accounts and delete a calendar account by ID.
  • Calendar Events: Route that exposes HTTP methods to CRUD calendar events.
  • Calendars: Route that exposes HTTP methods to update calendars.

A route definition using tRPC would be something like this:

export const calendarEventsRouter = createTRPCRouter({
  getCalendarEvent: publicProcedure
    .input(
      z.object({
        endUserAccountId: z.string(),
        calendarId: z.string(),
        eventId: z.string(),
      }),
    )
    .query(async ({ ctx, input }) => {
      return await getCalendarEvent(
        input.endUserAccountId,
        input.calendarId,
        input.eventId,
      );
    }),
});

The getCalendarEvent method comes from the OneCal Unified Calendar API client we built above.

Please open the API routes path in our GitHub repo to get the contents of each API route, as pasting them in this article would be quite repetitive.

Building the Frontend

The frontend is going to be built using Next.js + TypeScript. When building a calendar app, the most important component is the …. you guessed it, the calendar.

Based on our experience, the best calendar ui libraries in Next.js and React are:

For this example, we chose to use react-big-calendar due to the ease of use with Next.js, but bear in mind that we’d recommend using fullcalendar in production applications, as it’s more customizable and has broader community support.

Furthermore, fullcalendar is available in other libraries like Zvelte, Vue.js, etc.

react-big-calendar usage:

      <Calendar
        culture="en-US"
        localizer={localizer}
        events={events}
        defaultView="week"
        eventPropGetter={eventPropGetter}
        components={components}
        onSelectSlot={(slotInfo) => {
          setCreateEventStart(slotInfo.start);
          setCreateEventEnd(slotInfo.end);
          setCreateEventOpen(true);
        }}
        onSelectEvent={(event) => {
          setSelectedEvent(event);
        }}
        selectable
        onRangeChange={(range) => {
          // Week view: range is array of dates
          if (Array.isArray(range) && range.length >= 2) {
            setDateRange([range[0]!, range[range.length - 1]!]);
            return;
          }
          // Month view: range is object with start/end
          if (
            range &&
            typeof range === "object" &&
            "start" in range &&
            "end" in range
          ) {
            setDateRange([range.start, range.end]);
            return;
          }
          // Day view: range is a single Date
          if (range instanceof Date) {
            setDateRange([range, range]);
            return;
          }
        }}
      />

To view the full implementation, please open the src/app/(protected)/(calendar) path on the GitHub repo. The main component is the events-calendar.tsx page. You can also find components for editing events (including recurring events), deleting events and creating events.

Here is how the Calendar looks like:

Calendar UI

The user can click on a cell and create an event:

Calendar App - Create event UI

When the user clicks on an existing event, they get the option to delete it or edit it.

Calendar App - Edit or Delete Event UI

When the event is recurring, the user gets the option to edit the selected instance, of the whole series.

Calendar App - Edit Recurring Event Popup

This the how the edit event UI looks like:

Calendar App- Edit Event UI

The UI needs polishing, but we didn’t want to build a perfect calendar app, as the purpose is to build a functional calendar app and calendar integration, you can take care of the styles and make sure they match your brand.

Common Challenges and Best Practices

Building a calendar app or adding calendar features is not always easy. Even if the main functionality seems simple, there are many small details that can cause issues later. Below are some common challenges you may face, and a few best practices to handle them.

1. Time Zones

Challenge:

Events may show up at the wrong time when users are in different time zones.

Best Practice:

  • Always save times in UTC in your database. This excludes calendar events, as we don't recommend storing calendar events in your database. When you fetch events via the calendar provider API, you should also be able to receive the event timezone from the API.
  • Convert to the user’s local time only when displaying on the frontend
  • Use a library like date-fns-tz or luxon to handle time conversions. In this example, we're using date-fns and date-fns-tz

2. Recurring Events

Challenge:

Handling events that repeat (daily, weekly, monthly) can be complex, especially when users want to edit or delete one instance.

Best Practice:

  • Let the user choose whether to update one event or the whole series. This practice is followed by Google Calendar, Outlook, and many other calendar clients. We also followed this practice in our Calendar app.

3. OAuth Token Expiration

Challenge:

Users may lose connection to their calendars if tokens expire or are revoked.


Best Practice:

  • Store refresh tokens securely to get new access tokens automatically.
  • Handle token errors gracefully and prompt users to reconnect their accounts if needed.

4. Keeping Data in Sync

Challenge:

Calendar data may become outdated if you only fetch it once.

Best Practice:

  • Use webhooks from OneCal Unified Calendar API to stay updated when events change.
  • We'd recommend you fetch events from calendar providers when the user interacts with the calendar app. Please don't store events in your database, as keeping them in sync with all calendar providers can be a difficult challenge. Furthermore, storing calendar events in the database is not that beneficial, considering that you can fetch calendar events from all providers using OneCal Unified Calendar API.

5. Handling API Errors

Challenge:

External APIs (Google, Outlook, iCloud) can return errors, rate limits, or temporary failures.


Best Practice:

  • Add retry logic for temporary errors (using timeouts is a viable option).
  • Respect API rate limits and back off when needed. Note that the OneCal Unified calendar API has rate limits, as well as the calendar providers like Google Calendar / Outlook Calendar.
  • Log all failed requests for easier debugging.

6. Large Calendars

Challenge:

Some users have hundreds or thousands of events, which can slow down the app.


Best Practice:

  • Load events in pages (use pagination). Pagination is supported by all major calendar providers. If you use OneCal Unified Calendar API, you'll notice that all results are paginated, so you won't run into this problem.
  • Only fetch events for the visible date range (for example, this week or month). It's generally a best practice to get as much data as you need. A calendar app has a day view, week view, month view, and year view. Please fetch events according to the time frame.

7. User Privacy and Security

Challenge:

Calendar data often includes private information.


Best Practice:

  • Don't store calendar events in your database. Storing the access key and refresh key should be enough.
  • Encrypt tokens and sensitive fields in your database. We'd recommend you encrypt your database at rest. Services like AWS RDS offer data encryption out of the box.
  • Allow users to disconnect their calendar accounts anytime. This is very important, as if you don't allow users to disconnect or delete their calendars, they'll revoke access straight from their Google Account management.

FAQ

1. Can I use a different backend instead of Next.js API routes?

Yes. While this example uses Next.js API routes with tRPC, you can use any backend framework like Nest.js, Express, or Django.

The key part is that your backend must communicate with the OneCal Unified Calendar API using HTTPS requests. The database structure and API logic will remain mostly the same.

2. Do I need to create my own Google or Microsoft developer apps?

No, you can use the OneCal Unified Google and Microsoft clients during development.

When your app goes live, you can create your own OAuth2 credentials if you want full control and branding for your users.

3. Can I use a different database instead of PostgreSQL?

Yes. Prisma supports many databases such as MySQL, SQLite, and MongoDB.

We chose PostgreSQL because it’s reliable, scalable, and easy to set up for production. You can also choose any other database and ORM, depending on your tech stack.

4. Is OneCal Unified Calendar API free to use?

You can start for free by creating an account at OneCal Unified.

There is a free tier that’s great for testing and small projects. For production use, you can upgrade depending on your usage and the number of connected accounts.

5. What if a user disconnects their calendar?

When a user disconnects, the app should remove the corresponding CalendarAccount and Calendars from your database.

You can keep local data for analytics if needed, but make sure not to sync or access disconnected calendars anymore.

7. Can I add notifications or reminders?

Yes. You can build reminders in your app or use the native notification system of the connected calendar (Google, Outlook, etc.).

8. What if the API rate limits my requests?

The OneCal Unified API includes built-in rate limits for stability. If you reach the limit, back off and retry after a short delay. Note that the calendar providers will also have their own application-level rate limits. These limits may vary and both Google and Microsoft have the option to request a higher limit if necessary.

9. Is it possible to sync events in both directions?

Yes. The OneCal Unified API supports two-way sync, meaning you can both read and write events to connected calendars.

You’ll receive webhook notifications when changes happen on the provider side.

10. How can I deploy this project?

You can deploy the app easily to Vercel.

Make sure to set your environment variables (DATABASE_URL, ONECAL_UNIFIED_API_KEY, OAuth credentials, etc.) in your project settings.

You can also containerize it using Docker if you prefer more control.