Claude Codeで通知システムを構築する方法
Claude Codeを使ってトースト通知、プッシュ通知、リアルタイム通知システムを効率的に構築。実践的なコード例とアーキテクチャ設計を解説。
通知システムの構築をClaude Codeで効率化する
Webアプリにおける通知は、トースト表示からプッシュ通知、リアルタイム更新まで多岐にわたります。Claude Codeを使えば、これらを統合した通知基盤を短時間で構築できます。
トースト通知コンポーネントの実装
まずはアプリ内で使うトースト通知を作ります。
> ライブラリを使わずにトースト通知コンポーネントをReactで作って。
> success/error/warning/infoの4種類。自動で消える。スタック表示対応。
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
type ToastType = "success" | "error" | "warning" | "info";
interface Toast {
id: string;
type: ToastType;
message: string;
}
interface ToastContextType {
addToast: (type: ToastType, message: string) => void;
}
const ToastContext = createContext<ToastContextType | null>(null);
const TOAST_STYLES: Record<ToastType, string> = {
success: "bg-green-500 text-white",
error: "bg-red-500 text-white",
warning: "bg-yellow-500 text-black",
info: "bg-blue-500 text-white",
};
const TOAST_ICONS: Record<ToastType, string> = {
success: "✓",
error: "✕",
warning: "⚠",
info: "ℹ",
};
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((type: ToastType, message: string) => {
const id = crypto.randomUUID();
setToasts((prev) => [...prev, { id, type, message }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
<div className="fixed bottom-4 right-4 flex flex-col gap-2 z-50">
{toasts.map((toast) => (
<div
key={toast.id}
className={`px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 animate-slide-in ${TOAST_STYLES[toast.type]}`}
>
<span>{TOAST_ICONS[toast.type]}</span>
<span>{toast.message}</span>
<button
onClick={() => setToasts((prev) => prev.filter((t) => t.id !== toast.id))}
className="ml-2 opacity-70 hover:opacity-100"
>
✕
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error("useToast must be used within ToastProvider");
return context;
}
WebSocketによるリアルタイム通知
サーバーからのリアルタイム通知を受け取る仕組みも構築できます。
セキュリティ警告:
ws://host?userId=xxxのようにクライアントが申告したuserIdをサーバーが無検証で信じる実装を、ネット上のサンプルコードでよく見かけます。これは 重大な脆弱性 で、誰でも他人のuserIdを指定するだけで他ユーザー宛の通知を盗聴できてしまいます。必ず サーバー側で発行された署名付きトークン(JWT など) を検証し、トークンからuserIdを取り出してください。クエリ文字列のuserIdは信用してはいけません。
// サーバー側(Node.js + ws + jsonwebtoken)
import { WebSocketServer, WebSocket } from "ws";
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET!;
if (!JWT_SECRET) throw new Error("JWT_SECRET is required");
interface Client {
ws: WebSocket;
userId: string; // 必ずJWTから取り出した検証済みの値を入れる
}
const clients = new Set<Client>();
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws, req) => {
try {
// 1) まず Authorization ヘッダ(Sec-WebSocket-Protocol ヘッダ経由でも可)
// から Bearer トークンを取り出す。
// 2) ヘッダが無い環境のためのフォールバックとしてクエリの ?token=... を受ける。
// ※ userId ではなくトークン自身をクエリで渡すのがポイント。
const authHeader = req.headers["authorization"];
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
const token =
(typeof authHeader === "string" && authHeader.startsWith("Bearer ")
? authHeader.slice("Bearer ".length)
: null) ?? url.searchParams.get("token");
if (!token) {
ws.close(4401, "Unauthorized: token required");
return;
}
// JWT検証。失敗時は例外。
const payload = jwt.verify(token, JWT_SECRET) as { sub?: string };
const userId = payload.sub;
if (!userId) {
ws.close(4401, "Unauthorized: invalid token");
return;
}
const client: Client = { ws, userId };
clients.add(client);
ws.on("close", () => clients.delete(client));
} catch (err) {
ws.close(4401, "Unauthorized");
}
});
// 特定ユーザーへの通知送信(userIdはサーバー内部で既に検証済み)
export function sendNotification(userId: string, notification: object) {
for (const c of clients) {
if (c.userId === userId && c.ws.readyState === WebSocket.OPEN) {
c.ws.send(JSON.stringify(notification));
}
}
}
クライアント側では、再接続時に 指数バックオフ を入れて、障害時にサーバーへ一気に負荷をかけないようにします。トークンは localStorage ではなく、サーバーから HttpOnly クッキーで配布するのが理想ですが、WebSocket のハンドシェイクは同一オリジンなら自動的にクッキーを送るため、その場合はクエリに token を付ける必要すらありません。
// クライアント側のフック
import { useEffect, useRef, useCallback } from "react";
interface Notification {
id: string;
type: string;
title: string;
body: string;
}
const MAX_RETRY_DELAY_MS = 30_000;
const BASE_RETRY_DELAY_MS = 1_000;
export function useNotifications(
token: string,
onNotification: (n: Notification) => void,
) {
const wsRef = useRef<WebSocket | null>(null);
const retryRef = useRef(0);
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const stoppedRef = useRef(false);
const connect = useCallback(() => {
if (stoppedRef.current) return;
// token を query で渡す(userId ではなく、サーバーが署名したJWT)
const ws = new WebSocket(
`wss://api.example.com/notifications?token=${encodeURIComponent(token)}`,
);
wsRef.current = ws;
ws.onopen = () => {
retryRef.current = 0; // 成功したらバックオフをリセット
};
ws.onmessage = (event) => {
try {
onNotification(JSON.parse(event.data));
} catch {
/* 無視 */
}
};
ws.onclose = (event) => {
// 4401(認証エラー)は再接続してもムダなので停止
if (event.code === 4401) {
stoppedRef.current = true;
return;
}
if (stoppedRef.current) return;
// exponential backoff + jitter
const attempt = retryRef.current++;
const delay =
Math.min(BASE_RETRY_DELAY_MS * 2 ** attempt, MAX_RETRY_DELAY_MS) +
Math.random() * 500;
reconnectTimerRef.current = setTimeout(connect, delay);
};
ws.onerror = () => {
ws.close();
};
}, [token, onNotification]);
useEffect(() => {
stoppedRef.current = false;
connect();
return () => {
stoppedRef.current = true;
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
wsRef.current?.close();
};
}, [connect]);
}
このクライアントは次の性質を満たします。
- 再接続間隔は 1s → 2s → 4s → 8s … と指数関数的に伸び、最大30秒でサチる
- jitter(±0.5秒)を加え、サーバ再起動直後に全クライアントが同時再接続するサンダリングハード問題を避ける
- サーバーから認証失敗(close code 4401)を返された場合は再接続をあきらめる
- 接続成功時にバックオフカウンタをリセット
通知の永続化と既読管理
通知をDBに保存して既読管理する機能も追加できます。
// Prismaスキーマから自動でAPI生成を依頼
// schema.prisma の内容をClaude Codeが読み取って実装
import { db } from "@/lib/database";
export async function getNotifications(userId: string) {
return db.notification.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
take: 50,
});
}
export async function markAsRead(notificationId: string) {
return db.notification.update({
where: { id: notificationId },
data: { readAt: new Date() },
});
}
export async function getUnreadCount(userId: string) {
return db.notification.count({
where: { userId, readAt: null },
});
}
Claude Codeでの開発効率を上げるには生産性を3倍にする10のTipsを参考にしてください。フォームとの連携についてはフォームバリデーション設計も併せてご覧ください。
まとめ
Claude Codeを使えば、トースト通知、リアルタイム通知、永続化まで含めた通知システム全体を効率的に構築できます。自然言語で要件を伝え、段階的に機能を追加していくのがおすすめです。
公式ドキュメントはClaude Codeから確認できます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
現役DX室長|Claude Code でゼロから多言語AI技術メディア運営中。実務直結の自動化、AI開発相談・研修受付中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code/Codexの安全なAgent Harness実装ガイド|権限・検証・ロールバックまで
AIエージェントを安全に動かす足場を、権限設定、実行計画、検証、ロールバックの4層で実装。Claude Code/Codexの実務例付き。
Claude Code サブエージェント活用パターン10選
Claude Code のサブエージェント機能を使いこなす実践パターンを10個紹介。並列処理、専門化、コンテキスト分離で開発効率を倍増させる方法を解説します。
Claude Code Agent SDK入門 ― 自律エージェントを最速で構築する方法
Claude Code Agent SDKを使って自律型AIエージェントを構築する方法を解説。セットアップからツール定義、マルチステップ実行まで実践コード付きで紹介します。