カレンダーアプリの作り方 - 包括的なガイド
公開日
アプリケーションにカレンダーを統合する必要がある開発者の方、あるいはカレンダーアプリやカレンダー統合機能を持つプロダクトの開発を検討している創業者の方に向けて、この記事を書きました。
カレンダーアプリをどのように構築するか、各判断を一歩ずつ進めながら、適切な技術を選び、主な課題とベストプラクティスを見ていきましょう。
この記事の範囲とは?
この記事の目的は、完全なカレンダーアプリを構築する方法、または既存のアプリケーションにカレンダープロバイダを統合する方法を紹介することです。ユーザーが自分のカレンダーを接続し、あなたのプロダクトを通してそれらを管理できるようにすることを目指します。
また、カレンダーのインターフェースをまったく表示せずにカレンダープロバイダと連携したい場合にも、この記事は役立ちます。たとえば、タスク管理アプリ、デーティングアプリ、またはユーザーのカレンダーにイベントを追加するだけの機能などが該当します。
この記事の一部を自分のコードベースで自由に再利用してください。技術選択、統一 API のアプローチ、コードの一部など、必要な箇所を活用できます。
アーキテクチャと技術スタックの概要
この例では Web をプラットフォームとして選びました。立ち上げが最も速く、カレンダーライブラリに関するコミュニティサポートが豊富だからです。
使用するプログラミング言語は TypeScript、Web フレームワークは Next.js です。
以下は、使用する技術スタックの概要です。
- フロントエンド : Next.js(App Router、TypeScript)
- バックエンド : Next.js API Routes + tRPC
- データベース : PostgreSQL(ORM として
Prismaを使用) - カレンダー API : Apiroc 統合カレンダー API
- ホスティング : プロバイダに依存しません。Vercel や任意の環境でホスティングできます。
- 認証 : OAuth2(Google、Microsoft)を
better-auth経由で利用
以下は、技術スタックの各要素がどのように連携するかを示したアーキテクチャ図です。
定義:
- クライアント (1) : エンドユーザーのデバイスです。図ではモバイルデバイスを使用していますが、デスクトップやラップトップなど、Web ブラウザ(Google Chrome など)を備えた任意のデバイスが対象です。クライアントは Next.js フロントエンドをレンダリングし、ユーザー操作を処理し、安全な HTTPS リクエストでバックエンドと通信します。
- Web サーバー (2) : Web サーバーはユーザーインターフェースをホストし、Next.js(App Router)で構築された Web アプリケーションを提供します。最適化された HTML、CSS、JavaScript をクライアントに配信し、SSR(サーバーサイドレンダリング)や ISR(インクリメンタル静的再生成)を通じて高速なパフォーマンスと SEO 効果を実現します。
- Next.js API (3) : Next.js API は、同一アプリケーション内で API Routes と tRPC を用いて実装されたバックエンド層です。フロントエンド、データベース、そして Apiroc 統合カレンダー API のような外部統合をつなぐ中心的なハブです。従来の REST エンドポイントとは異なり、tRPC はフロントエンドとバックエンド間の型安全な通信を、別途 API スキーマを定義することなく可能にします。クライアントは TypeScript の型推論を保ったままバックエンドの手続きを直接呼び出せるため、開発速度が向上し、実行時エラーが減ります。
- PostgreSQL (4) : PostgreSQL データベースは、ユーザー、セッション、接続されたカレンダーアカウントなど、すべての永続的なアプリケーションデータを保存します。ユーザー関連データおよび同期状態の記録システムとして機能します。Prisma を ORM 層として利用することで、スキーマはデータベースにきれいにマッピングされ、マイグレーションやクエリが容易になります。
- Apiroc 統合カレンダー API (5) : Apiroc は、すべてのカレンダープロバイダを 1 つの標準化されたインターフェースで統合するために使用する API です。これにより、プロバイダごとに個別の実装を書いたり、異なるデータ形式を処理したり、複数の統合を維持したり、プロバイダの API 変更に追従したりする必要がなくなります。アプリケーションは、API キーと操作対象のカレンダー情報(イベントやカレンダーの CRUD 操作など)を Apiroc に送信します。プロバイダが何であっても同じです。Apiroc が各カレンダープロバイダと通信し、すべてのプロバイダで共通フォーマットのレスポンスを返します。

なお、Web サーバー (2) と Next.js API (3) は同じサーバー上(Vercel や Docker など)で実行できます。図では UI サーバーと API サーバーがあることを分かりやすくするために分けて表現していますが、実際には同一の Next.js コードベース内にあり、通常は同じサーバーでホストされます。
データモデルの設計
アーキテクチャと技術スタックが明確になったので、次はデータモデルを定義します。ORM には Prisma を使用します。
この Prisma スキーマは、ユーザー認証、カレンダーアカウント接続(Google、Microsoft)、および統一 API を介したイベント同期をサポートする基本的なカレンダーアプリケーションのデータ構造を定義しています。
主要なモデルは以下の通りです。
1. User(ユーザー)
アプリのエンドユーザーを表します。各ユーザーは複数のセッション、接続されたアカウント(OAuth)、およびカレンダーアカウントを持つことができます。email、name、onboardingCompletedAt といったフィールドでプロフィールやオンボーディングの進行状況を追跡します。
2. CalendarAccount(カレンダーアカウント)
リンクされた外部カレンダーアカウント(例: Google または Microsoft アカウント)を表します。provider、email、status(アクティブまたは期限切れ)を保存します。各 CalendarAccount は 1 人の User に属し、複数の Calendar エントリを持つことができます。
3. Calendar(カレンダー)
リンクされたアカウント内の個別カレンダー(例: 「仕事」「個人」「家族」など)を表します。name、color、timezone などの表示フィールド、および isPrimary や isReadOnly といったフラグを含みます。各カレンダーは、対応する User と CalendarAccount の両方に関連付けられます。
4. Account(アカウント)
OAuth プロバイダデータ(Google または Microsoft)を管理します。アクセストークンやリフレッシュトークン、有効期限、スコープ情報を保持し、認証およびカレンダー同期に使用します。
5. Session(セッション)
ユーザーのアクティブなログインセッションを追跡します。token、expiresAt、ipAddress、userAgent などのフィールドを持ち、セッションの管理とセキュリティを確保します。
6. Verification(検証)
メールログインのマジックリンクやパスワードレス認証コードなどの一時的な検証に使用されます。一時的な識別子と有効期限を保存します。
7. Enum(列挙型)
CalendarAccountProvider: サポートされるプロバイダを定義します(GOOGLE、MICROSOFT)。CalendarAccountStatus: 接続されたアカウントがACTIVE(有効)かEXPIRED(期限切れ)かを追跡します。
データベース ER 図:
サンプルリポジトリ内の schema.prisma ファイルを開くと、型やリレーションを含む完全なデータベーススキーマを確認できます。
バックエンドの構築
前述のとおり、API の構築には Next.js の API Routes を使用します。UI と API が同じコードベース内にあり、同じサーバー上でホストできるのは非常に便利です。両方を同時に実行できるからです。
認証の構築
認証フレームワークとして better-auth を使用します。Better Auth を使うと、認証処理を簡単かつスムーズに実装できます。
Next.js への統合手順については、Better Auth のガイドを参照してください。ほぼ同じステップで実装できます。
詳しくは、サンプルリポジトリ内の auth ファイル を確認してください。
すべてのカレンダープロバイダと通信するための Apiroc 統合カレンダー API の設定
カレンダーアプリを構築する際、または既存のプロダクトにカレンダーを追加する際の最大の課題は、プロバイダごとに異なる API に対応することです。各 API を個別に学び、異なるデータ構造・リクエスト・レスポンスに対応するには多くの時間がかかります。さらに、プロバイダごとに別々の統合を構築し、開発完了後もすべてを維持しなければなりません。
この問題の良い解決策は、統一されたカレンダー API を利用することです。これにより、すべてのカレンダープロバイダを単一の標準化された API で統合できます。この例では、Apiroc 統合カレンダー API を使用します。
Apiroc を使い始めるには、次の手順に従ってください。
- まず、Apiroc にサインアップし、無料アカウントを作成します。
- サインアップ後、統合したいカレンダープロバイダを有効化します。 Google Calendar と Outlook の両方を有効化することをお勧めします。統一カレンダー API 製品の利点が分かりやすくなります。 サンドボックスおよび開発環境では、Google や Microsoft のクライアントを自分で作成する必要はありません。Apiroc の Google および Microsoft クライアントを利用して、Google Calendar または Outlook アカウントをアプリケーションに接続できます。
- API キーを作成し、環境変数
APIROC_API_KEYに保存します。
Apiroc API クライアントの構築
Apiroc を設定し、API キーを取得したら、次は Apiroc 統合カレンダー API と通信する API クライアントを構築します。
import { env } from "@/env";
import type {
EndUserAccount,
PaginatedResponse,
UnifiedCalendar,
UnifiedEvent as UniversalEvent,
} from "@/server/lib/apiroc/types";
import ky from "ky";
export const apirocApi = ky.create({
prefixUrl: env.NEXT_PUBLIC_APIROC_URL,
headers: {
"x-api-key": env.APIROC_API_KEY,
},
});
export async function getEndUserAccountById(id: string) {
const response = await apirocApi.get<EndUserAccount>(
`endUserAccounts/${id}`,
);
return response.json();
}
export async function getCalendarsForEndUserAccount(endUserAccountId: string) {
const response = await apirocApi.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 apirocApi.get<
PaginatedResponse<UniversalEvent>
>(`events/${endUserAccountId}/${calendarId}?${queryParams}`);
return response.json();
}
export async function getCalendarEvent(
endUserAccountId: string,
calendarId: string,
eventId: string,
) {
const response = await apirocApi.get<UniversalEvent>(
`events/${endUserAccountId}/${calendarId}/${eventId}`,
);
return response.json();
}
export async function createCalendarEvent(
endUserAccountId: string,
calendarId: string,
event: Partial<UniversalEvent>,
) {
const response = await apirocApi.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 apirocApi.put<UniversalEvent>(
`events/${endUserAccountId}/${calendarId}/${eventId}`,
{json: event},
);
return response.json();
}
export async function deleteCalendarEvent(
endUserAccountId: string,
calendarId: string,
eventId: string,
) {
await apirocApi.delete(
`events/${endUserAccountId}/${calendarId}/${eventId}`,
);
}
クライアントの型定義を自分で用意する必要はありません。公式の Node.js SDK(@apiroc/unified-calendar-api-node-sdk)をインストールすれば、エンドユーザーアカウント、カレンダー、イベントの型をそのままインポートできます。SDK にはすぐに使えるクライアントも含まれているため、上記の関数はさらに短く書けます。
以下のシーケンス図は、サンプルのカレンダーアプリが Apiroc 統合カレンダー API とどのように連携して、すべてのカレンダープロバイダと統合しているかを示しています。
API ルートの作成
Apiroc API クライアントが用意できたら、カレンダーアカウントとカレンダーイベントを管理するための API ルートを作成します。 セッション API を自分で作成する必要はありません。Better Auth が処理してくれます。
API には以下のルート定義があります:
- カレンダーアカウント : すべてのカレンダーアカウントを一覧表示し、ID で削除できる HTTP メソッドを提供します。
- カレンダーイベント : カレンダーイベントを CRUD 操作する HTTP メソッドを提供します。
- カレンダー : カレンダーを更新する HTTP メソッドを提供します。
tRPC を使用したルート定義の例:
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,
);
}),
});
getCalendarEvent メソッドは、上で構築した Apiroc API クライアントのものです。
各 API ルートの内容については、サンプルリポジトリの API ルートフォルダ を参照してください。この記事内で全コードを貼り付けると冗長になるため、詳細はリポジトリをご確認ください。
フロントエンドの構築
フロントエンドは Next.js と TypeScript を使用して構築します。 カレンダーアプリを作る際、最も重要なコンポーネントは、そう、カレンダーそのものです。
私たちの経験上、Next.js や React で使える優れたカレンダー UI ライブラリは以下の通りです。
この例では、Next.js との相性の良さから react-big-calendar を使用していますが、本番環境では fullcalendar をおすすめします。fullcalendar はよりカスタマイズ性が高く、コミュニティサポートも充実しています。
さらに、fullcalendar は Svelte や Vue.js など他のフレームワークでも利用できます。
react-big-calendar の使用例
<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;
}
}}
/>
完全な実装を確認するには、GitHub リポジトリ内の src/app/(protected)/(calendar) ディレクトリを開いてください。
メインコンポーネントは events-calendar.tsx ページです。イベントの編集(繰り返しイベントを含む)、削除、作成のためのコンポーネントも含まれています。
カレンダーはこのように表示されます。
ユーザーはセルをクリックしてイベントを作成できます。
既存のイベントをクリックすると、削除または編集ができます。
繰り返しイベントの場合、ユーザーは「このインスタンスのみ」または「シリーズ全体」のどちらを編集するかを選択できます。
こちらはイベント編集 UI の例です。
UI には改善の余地がありますが、このプロジェクトの目的は完璧なカレンダーアプリを作ることではありません。目的は、動作するカレンダーアプリと動作するカレンダー統合を構築することです。デザインやスタイルはあなたのブランドに合わせて自由にカスタマイズしてください。
一般的な課題とベストプラクティス
カレンダーアプリを構築したり、カレンダー機能を追加したりすることは、必ずしも簡単ではありません。 主な機能はシンプルに見えても、後から問題を引き起こす小さな詳細がたくさんあります。 以下では、よくある課題と、それに対処するためのベストプラクティスをいくつか紹介します。
1. タイムゾーン
課題:
ユーザーが異なるタイムゾーンにいる場合、イベントが誤った時刻に表示されることがあります。
ベストプラクティス:
- データベース内の時刻は常に UTC で保存します。 ただし、カレンダーイベントは例外です。カレンダーイベント自体をデータベースに保存することは推奨しません。 カレンダープロバイダの API からイベントを取得する際に、イベントのタイムゾーン情報も受け取れます。
- フロントエンドで表示する際にのみ、ユーザーのローカル時刻に変換します。
- タイムゾーン変換には
date-fns-tzやluxonなどのライブラリを使用します。この例ではdate-fnsとdate-fns-tzを使用しています。
2. 繰り返しイベント
課題:
毎日、毎週、毎月などの繰り返しイベントを扱うのは複雑です。 特に、ユーザーが特定のインスタンスだけを編集または削除したい場合、処理が難しくなります。
ベストプラクティス:
- ユーザーに「このイベントのみ」または「シリーズ全体」を更新する選択肢を与えます。 Google カレンダーや Outlook など多くのカレンダークライアントもこの方法を採用しています。 サンプルアプリでも同じ仕組みを採用しました。
3. OAuth トークンの有効期限
課題:
トークンが期限切れになったり取り消されたりすると、ユーザーのカレンダー接続が失われる可能性があります。
ベストプラクティス:
- リフレッシュトークンを安全に保存し、新しいアクセストークンを自動で取得できるようにします。
- トークンエラーが発生した場合は適切に処理し、必要に応じてユーザーに再接続を促します。
4. データの同期維持
課題:
カレンダーデータを一度だけ取得していると、古くなってしまいます。
ベストプラクティス:
- Apiroc 統合カレンダー API の Webhook を使用して、Google および Microsoft のカレンダーでイベントが変更されたときに通知を受け取ります。

- ユーザーがカレンダーアプリを操作したタイミングで、イベントをプロバイダから取得するのがおすすめです。 イベントを自分のデータベースに保存することは推奨しません。 すべてのカレンダープロバイダとの同期を維持するのは難しい問題だからです。 さらに、必要なときに Apiroc を通じてすべてのプロバイダからイベントを取得できるため、ローカルに保存するメリットはほとんどありません。
5. API エラーの処理
課題:
外部 API(Google、Outlook、iCloud など)は、エラーやレート制限、一時的な障害を返すことがあります。
ベストプラクティス:
- 一時的なエラーには再試行ロジックを追加します(タイムアウト処理が有効です)。
- API のレート制限を遵守し、必要に応じてバックオフ処理を実装します。 Apiroc にもレート制限があり、Google Calendar や Outlook Calendar などの各プロバイダにも独自の制限があります。
- すべての失敗リクエストをログに記録し、デバッグを容易にします。
6. 大規模なカレンダー
課題:
一部のユーザーは数百、数千のイベントを持っており、アプリのパフォーマンスが低下する可能性があります。
ベストプラクティス:
- イベントをページごとにロードします(ページネーション)。 主要なカレンダープロバイダはすべてページネーションをサポートしています。 Apiroc を使用する場合、すべての結果がページネートされているため、この問題は発生しません。
- 表示中の日付範囲(例: 今週または今月)のイベントのみを取得します。 原則として、必要なデータだけを取得してください。カレンダーアプリには「日」「週」「月」「年」ビューがあるため、表示中の範囲に応じてイベントを取得しましょう。
7. ユーザープライバシーとセキュリティ
課題:
カレンダーデータには個人的な情報が含まれることが多いです。
ベストプラクティス:
- カレンダーイベントをデータベースに保存しないようにします。 アクセストークンとリフレッシュトークンを保存するだけで十分です。
- データベース内のトークンや機密フィールドを暗号化します。 データベースの保存時暗号化も推奨します。AWS RDS などのサービスでは標準でデータ暗号化が利用できます。
- ユーザーがいつでも自分のカレンダーアカウントを切断できるようにします。 これは非常に重要です。アプリ内でカレンダーを切断または削除できない場合、ユーザーは Google アカウントの設定から直接アクセス権を取り消すことになります。
よくある質問(FAQ)
1. Next.js API Routes の代わりに別のバックエンドを使えますか?
はい。 この例では Next.js API Routes と tRPC を使用していますが、Nest.js、Express、Django など、任意のバックエンドフレームワークを使用できます。 重要なのは、バックエンドが Apiroc 統合カレンダー API と HTTPS 経由で通信することです。 データベース構造や API ロジックはほとんど同じままです。
2. 自分で Google や Microsoft の開発者アプリを作成する必要がありますか?
いいえ。 開発中は Apiroc の Google および Microsoft クライアントを使用できます。 本番環境に移行する際には、独自の OAuth2 資格情報を使用します。これにより、ユーザーに対するブランド表示とアクセス制御を完全にコントロールできます。
3. PostgreSQL の代わりに別のデータベースを使えますか?
はい。 Prisma は MySQL、SQLite、MongoDB など多くのデータベースをサポートしています。 私たちが PostgreSQL を選んだ理由は、信頼性が高くスケーラブルであり、本番環境でのセットアップが簡単だからです。 もちろん、技術スタックに応じて他のデータベースや ORM を選択しても構いません。
4. Apiroc 統合カレンダー API は無料で使えますか?
はい。 Apiroc アカウントを作成すれば無料で利用を開始できます。クレジットカードは不要です。 無料プランには最大 10 件のエンドユーザーアカウントが含まれ、テストや小規模プロジェクトに最適です。 本番運用では、Pro プラン(月額 25 ドル、エンドユーザーアカウント 50 件込み、以降は 1 件あたり月額 0.50 ドル)にアップグレードできます。
5. ユーザーがカレンダー接続を解除した場合はどうすればいいですか?
ユーザーが接続を解除した際は、該当する CalendarAccount および Calendars のデータをデータベースから削除してください。
分析目的でローカルデータを保持することは可能ですが、切断されたカレンダーとの同期やアクセスは行わないようにしてください。
6. 通知やリマインダーを追加できますか?
はい。 アプリ内で独自のリマインダー機能を構築することも、接続されたカレンダー(Google、Outlook など)のネイティブ通知システムを利用することも可能です。
7. API がレート制限をかけてきた場合はどうすればいいですか?
Apiroc には安定性を保つためのレート制限があります(サンドボックスアプリは毎秒 20 リクエスト、本番アプリは毎秒 300 リクエスト)。 上限に達した場合は、しばらく待ってから再試行してください。 また、Google や Microsoft の各カレンダープロバイダにも独自のアプリケーションレベルのレート制限があります。 これらの制限はさまざまで、必要に応じてより高い制限をリクエストすることもできます。
8. イベントを双方向に同期することは可能ですか?
はい。 Apiroc はイベントの読み取りと書き込みの両方をサポートしています。 つまり、接続されたカレンダーからイベントを読み取るだけでなく、イベントの作成や更新も可能です。 Google および Microsoft のカレンダーでプロバイダ側の変更が発生した場合は、Webhook 通知を受け取ることができます。Apple iCloud カレンダーの Webhook は計画中で、まだ利用できません。
9. このプロジェクトをどのようにデプロイすればいいですか?
このアプリは Vercel に簡単にデプロイできます。
プロジェクト設定で DATABASE_URL、APIROC_API_KEY、OAuth 資格情報などの環境変数を設定してください。
また、より細かく制御したい場合は Docker を使用してコンテナ化することも可能です。
