カレンダーアプリの作り方 - 包括的なガイド
単一のAPIで複数のカレンダーを統合する
OneCal Unified Calendar API を使用すれば、複数のカレンダー提供サービスを素早くアプリに統合でき、保守やテストの心配はありません。
もしあなたがアプリケーションにカレンダーを統合する開発者であったり、カレンダーアプリを構築しようと考えている起業家、あるいはカレンダー統合機能を備えたアプリを開発している方であれば、この記事はあなたのためのものです。
カレンダーアプリをどのように構築するかを学び、各ステップを順に理解し、最適な技術を選択し、直面する課題やベストプラクティスを学んでいきましょう。
完全な実装やコードベースを確認したい場合は、GitHub 上の Unified Calendar View Example アプリをご覧ください。ローカルでアプリケーションを実行するための手順は README ファイルに記載されています。
この記事の範囲とは?
イントロダクションで述べたように、この記事の目的は、完全なカレンダーアプリを構築する方法、または既存のアプリケーションにカレンダープロバイダを統合する方法を紹介することです。ユーザーが自身のカレンダーを接続し、あなたのアプリを通してそれらを管理できるようにすることを目指します。
また、この記事はカレンダーインターフェースを提供せずにカレンダープロバイダと統合したい場合にも役立ちます。たとえば、タスク管理アプリ、デーティングアプリ、またはユーザーのカレンダーにイベントを挿入する機能などが該当します。
この記事の一部をコードベースに組み込むことも自由です。技術選択、統一 API、またはコードの一部など、必要な箇所を活用してください。
すべてのカレンダープロバイダを単一の API で統合する場合はこちらの OneCal Unified Calendar APIをご覧ください。
アーキテクチャと技術スタックの概要
この例で使用するプラットフォームは Web です。理由は、立ち上げが容易であり、カレンダーライブラリに関するコミュニティサポートが豊富だからです。
使用するプログラミング言語は TypeScript、Web フレームワークは Next.js です。
以下は、使用する技術スタックの概要です。
- フロントエンド: Next.js(App Router、TypeScript)
- バックエンド: Next.js API Routes + tRPC
- データベース: PostgreSQL(ORM として
Prismaを使用) - カレンダー API: OneCal Unified Calendar 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 を用いて実装されたバックエンド層として機能します。これはフロントエンド、データベース、外部統合(例: OneCal Unified Calendar API)をつなぐ中心的なハブです。従来の REST エンドポイントとは異なり、tRPC はフロントエンドとバックエンド間の型安全な通信を可能にし、別途 API スキーマを定義する必要がありません。これにより、クライアントは TypeScript の型補完を保ったままバックエンドの手続きを直接呼び出すことができ、開発速度の向上と実行時エラーの削減につながります。
PostgreSQL (4): PostgreSQL データベースは、ユーザー、セッション、接続されたカレンダーアカウントなど、すべての永続的なアプリケーションデータを保存します。ユーザー関連データおよび同期状態の記録システムとして機能します。Prisma を ORM 層として利用することで、スキーマはデータベースにきれいにマッピングされ、マイグレーションやクエリが容易になります。
OneCal Unified Calendar API (5): OneCal Unified Calendar API は、すべてのカレンダープロバイダを統一された API で統合するための API です。OneCal Unified を使用することで、複数のカレンダープロバイダに対する個別の実装や異なるデータ形式の処理、複数の統合の維持、API 変更への対応などを心配する必要がなくなります。アプリケーションは、API キーと操作対象のカレンダー情報を OneCal Unified Calendar API に送信し(イベントやカレンダーの CRUD 操作など)、OneCal が各カレンダープロバイダと通信して、すべてのプロバイダで共通フォーマットのレスポンスを返します。
注記: 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 ファイル(GitHub リポジトリ内)を開くと、すべてのデータベーススキーマ、型、リレーションを確認できます。
バックエンドの構築
前述のとおり、API の構築には Next.js の API Routes を使用します。UI と API が同じコードベース内に存在し、同じサーバー上でホストできるのは非常に便利です。これにより、UI と API を同時に実行できます。
Next.js の API Routes を使用する理由は、アプリの複雑さの観点から合理的だからです。今回構築するのは、ユーザーがログインし、自分のカレンダーを接続し、イベントの作成・更新・削除などの操作を行うシンプルなカレンダーアプリです。もし、より複雑なシステムを構築する場合は、Node.js、Nest.js、または他のフレームワークを使用しても問題ありません。データベースのエンティティは同じであり、すべてのカレンダープロバイダとやり取りする OneCal Unified Calendar API は HTTP ベースのため、どのプログラミング言語やフレームワークからでも呼び出せます。
また、Next.js の API Routes をそのまま使用するのではなく、tRPC を利用して API をエンドツーエンドで型安全にします。
認証の構築
認証フレームワークとして better-auth を使用します。Better Auth を使用すると、認証処理を非常に簡単かつスムーズに実装できます。
Next.js への統合手順については、Better Auth のガイドを参照してください。ほぼ同じステップで実装可能です。
詳しくは、リポジトリ内の auth ファイル を確認してください。
すべてのカレンダープロバイダと通信するための OneCal Unified Calendar API の設定
カレンダーアプリを構築する際、または既存アプリやプロダクト機能にカレンダーを統合する際の最大の課題は、プロバイダごとの異なる API に対応することです。各 API の学習や異なるデータ構造・リクエスト・レスポンスへの対応には多くの時間がかかります。さらに、各プロバイダごとに別の統合を構築し、完成後もそれを維持しなければなりません。
この問題を解決する優れた方法は、統一された Calendar API を利用することです。これにより、すべてのカレンダープロバイダを単一の標準化された API で統合できます。この例では、OneCal Unified Calendar API を使用します。
OneCal Unified のセットアップ手順
まず、OneCal Unified にサインアップ し、無料アカウントを作成します。
サインアップ後、統合したいカレンダープロバイダを有効化します。
Google Calendar と Outlook の両方を有効化することをお勧めします。これにより、統一カレンダー API 製品の利点を理解できます。
開発およびサンドボックス環境では、Google または Microsoft のクライアントを自分で作成する必要はありません。OneCal Unified の Google クライアントを利用することで、Outlook または Google Calendar アカウントをアプリケーションに接続できます。API キーを作成し、環境変数
ONECAL_UNIFIED_API_KEYに保存します。
OneCal Unified API クライアントの構築
OneCal Unified Calendar API を設定し、API キーを取得したら、次に API クライアントを構築します。これが 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}`,
);
}
クライアントの型定義やその他のクラスは、この GitHub パス にあります。
以下のシーケンス図は、Example Calendar App が OneCal Unified Calendar API とどのように連携して、すべてのカレンダープロバイダとのシームレスな統合を実現しているかを説明しています。
API ルートの作成
OneCal Unified Calendar API クライアントを設定したら、カレンダーアカウントやカレンダーイベントを管理するための API ルートを作成します。
セッション API を明示的に作成する必要はありません。BetterAuth を活用して処理します。
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 メソッドは、上で構築した OneCal Unified Calendar API クライアントから呼び出されます。
各 API ルートの内容については、API routes ディレクトリ を参照してください。この記事内で全コードを貼り付けると冗長になるため、詳細はリポジトリをご確認ください。
フロントエンドの構築
フロントエンドは 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 の例です。
UI には改善の余地がありますが、このプロジェクトの目的は「完璧なカレンダーアプリ」を作ることではなく、機能的なカレンダーアプリとカレンダー統合を構築することです。デザインやスタイルはあなたのブランドに合わせて自由にカスタマイズしてください。
一般的な課題とベストプラクティス
カレンダーアプリを構築したり、カレンダー機能を追加したりすることは、常に簡単ではありません。
主な機能はシンプルに見えても、多くの小さな問題が後から発生する可能性があります。
以下では、よくある課題と、それに対処するためのベストプラクティスを紹介します。
1. タイムゾーン
課題:
ユーザーが異なるタイムゾーンにいる場合、イベントが誤った時間に表示されることがあります。
ベストプラクティス:
- データベース内の時間は常に UTC で保存します。
ただし、カレンダーイベントは例外です。カレンダーイベントをデータベースに保存することは推奨しません。
カレンダープロバイダの API からイベントを取得する際に、イベントのタイムゾーン情報を受け取ることができます。 - フロントエンドで表示する際にのみ、ユーザーのローカルタイムに変換します。
- タイムゾーン変換には
date-fns-tzやluxonなどのライブラリを使用します。この例ではdate-fnsとdate-fns-tzを使用しています。
2. 繰り返しイベント
課題:
毎日、毎週、毎月などの繰り返しイベントを扱うのは複雑です。
特に、ユーザーが特定のインスタンスだけを編集または削除したい場合、処理が難しくなります。
ベストプラクティス:
- ユーザーに「このイベントのみ」または「シリーズ全体」を更新する選択肢を与えます。
Google カレンダーや Outlook などの主要なカレンダーアプリもこの方法を採用しています。
本記事のカレンダーアプリも同じ仕組みを採用しています。
3. OAuth トークンの有効期限
課題:
トークンが期限切れになったり、取り消された場合、ユーザーのカレンダー接続が失われる可能性があります。
ベストプラクティス:
- リフレッシュトークンを安全に保存し、新しいアクセストークンを自動で取得できるようにします。
- トークンエラーが発生した場合は、エラーを適切に処理し、ユーザーに再接続を促します。
4. データの同期維持
課題:
カレンダーデータを一度だけ取得していると、古くなってしまうことがあります。
ベストプラクティス:
- OneCal Unified Calendar API の Webhook を使用して、イベント変更時に最新情報を取得します。
- ユーザーがカレンダーアプリを操作したタイミングで、イベントをプロバイダから取得するのがおすすめです。
イベントを自分のデータベースに保存することは推奨しません。
複数のカレンダープロバイダ間で完全な同期を維持するのは困難だからです。
さらに、OneCal Unified Calendar API を使用すれば、すべてのプロバイダから直接イベントを取得できます。
5. API エラーの処理
課題:
外部 API(Google、Outlook、iCloud など)は、エラーやレート制限、一時的な障害を返すことがあります。
ベストプラクティス:
- 一時的なエラーには再試行ロジックを追加します(タイムアウト処理が有効です)。
- API のレート制限を遵守し、必要に応じてバックオフ処理を実装します。
OneCal Unified Calendar API にもレート制限がありますが、Google Calendar や Outlook Calendar などの各プロバイダにも独自の制限があります。 - すべての失敗リクエストをログに記録し、デバッグを容易にします。
6. 大規模なカレンダー
課題:
一部のユーザーは数百、数千のイベントを持っており、アプリのパフォーマンスが低下する可能性があります。
ベストプラクティス:
- イベントをページごとにロードします(ページネーション)。
主要なカレンダープロバイダはすべてページネーションをサポートしています。
OneCal Unified Calendar API を使用する場合、すべての結果がページネートされているため、この問題を回避できます。 - 表示中の日付範囲(例: 今週または今月)のみを取得します。
カレンダーアプリには「日」「週」「月」「年」ビューがあるため、それに応じた範囲のデータを取得するのが一般的なベストプラクティスです。
7. ユーザープライバシーとセキュリティ
課題:
カレンダーデータには機密情報が含まれることが多いです。
ベストプラクティス:
- カレンダーイベントをデータベースに保存しないようにします。
アクセストークンとリフレッシュトークンを安全に保存するだけで十分です。 - データベース内のトークンや機密情報を暗号化します。
データベースの暗号化(at-rest encryption)を推奨します。AWS RDS などのサービスでは標準で暗号化が利用できます。 - ユーザーがいつでも自分のカレンダーアカウントを切断できるようにします。
これを実装していない場合、ユーザーは Google アカウント管理から直接アクセス権を取り消すことになります。
よくある質問(FAQ)
1. Next.js API Routes の代わりに別のバックエンドを使えますか?
はい。
この例では Next.js API Routes と tRPC を使用していますが、Nest.js、Express、Django など、任意のバックエンドフレームワークを使用できます。
重要なのは、バックエンドが OneCal Unified Calendar API と HTTPS 経由で通信できることです。
データベース構造や API ロジックはほとんど同じままです。
2. 自分で Google や Microsoft の開発者アプリを作成する必要がありますか?
いいえ。
開発中は OneCal Unified の Google および Microsoft クライアントを使用できます。
本番環境でアプリを公開する際には、独自の OAuth2 資格情報を作成し、ブランドやアクセス制御をカスタマイズすることも可能です。
3. PostgreSQL の代わりに別のデータベースを使えますか?
はい。
Prisma は MySQL、SQLite、MongoDB など多くのデータベースをサポートしています。
私たちが PostgreSQL を選んだ理由は、信頼性が高くスケーラブルであり、プロダクション環境でのセットアップが簡単だからです。
もちろん、技術スタックに応じて他のデータベースや ORM を選択しても構いません。
4. OneCal Unified Calendar API は無料で使えますか?
はい。
OneCal Unified に登録すれば無料で利用を開始できます。
無料プランはテストや小規模プロジェクトに最適です。
本番運用では、利用量や接続アカウント数に応じて有料プランにアップグレードできます。
5. ユーザーがカレンダー接続を解除した場合はどうすればいいですか?
ユーザーが接続を解除した際は、該当する CalendarAccount および Calendars のデータをデータベースから削除してください。
分析目的でローカルデータを保持することは可能ですが、切断されたカレンダーとの同期やアクセスは行わないようにしてください。
6. 通知やリマインダーを追加できますか?
はい。
アプリ内で独自のリマインダー機能を構築することも、接続されたカレンダー(Google、Outlook など)のネイティブ通知システムを利用することも可能です。
7. API がレート制限をかけてきた場合はどうすればいいですか?
OneCal Unified API には安定性を保つためのレート制限があります。
上限に達した場合は、しばらく待ってから再試行してください。
また、Google や Microsoft の各カレンダープロバイダにも独自のアプリケーションレベルのレート制限があります。
必要に応じて、より高い制限をリクエストすることもできます。
8. イベントを双方向に同期することは可能ですか?
はい。
OneCal Unified API は双方向同期をサポートしています。
つまり、接続されたカレンダーからイベントを読み取るだけでなく、イベントの作成や更新も可能です。
さらに、プロバイダ側で変更が発生した場合は Webhook 通知を受け取ることができます。
9. このプロジェクトをどのようにデプロイすればいいですか?
このアプリは Vercel に簡単にデプロイできます。
プロジェクト設定で DATABASE_URL、ONECAL_UNIFIED_API_KEY、OAuth 資格情報などの環境変数を設定してください。
また、より細かく制御したい場合は Docker を使用してコンテナ化することも可能です。