Advanced

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から確認できます。

#Claude Code #通知システム #WebSocket #トースト #リアルタイム
無料プレゼント

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

現役DX室長|Claude Code でゼロから多言語AI技術メディア運営中。実務直結の自動化、AI開発相談・研修受付中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。