サーバー送信イベント(SSE)をNext.js App routerのAPIで実装してみた。ChatGPTのリアルタイムっぽい生成のやつ。

に公開

はじめに

SSEについて存在は知っているものの、試したことがないので自分で実装してみます。
SSEとはChatGPTでリアルタイムに生成している風のこれ↓です。

ブラウザのdevツールからネットワークタブを確認するとこの通り、順に取得していることが確認できます。

https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events

リアルタイム通信を実装する上で、WebSocketよりも実装は楽ですが単方向通信しかできなかったり、ドメインごとに接続の最大数が決まっている制限があります。
SSEのみだと単方向ですが通常のAPIと組み合わせてDB保存を行えば、多少のラグはありますがチャットなども実装可能です。

実行環境

  • Next.js@15.3.1
    app routerを使用
  • node@22.15.0(LTS)

本題

API

src/app/api/sse/route.ts
import { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  const { readable, writable } = new TransformStream();
  const writer = writable.getWriter();

  writer.write(
    new TextEncoder().encode("data: SSE connection established\n\n")
  );

  const interval = setInterval(() => {
    writer.write(
      new TextEncoder().encode(
        `data: Current time: ${new Date().toISOString()}\n\n`
      )
    );
  }, 1000);

  request.signal.addEventListener("abort", () => {
    clearInterval(interval);
    writer.close();
  });

  return new Response(readable, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

curlで確認する場合

curl -N http://localhost:3000/api/sse

data: SSE connection established

data: Current time: 2025-05-06T08:08:42.871Z

data: Current time: 2025-05-06T08:08:43.872Z

data: Current time: 2025-05-06T08:08:44.874Z

data: Current time: 2025-05-06T08:08:45.874Z

フロントエンド

コンポーネントとして作成しているので、適切にインポートして利用してください。

SSEClient.tsx
import { useEffect, useState } from 'react';

export default function SSEClient() {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const eventSource = new EventSource('/api/sse');

    eventSource.onmessage = (event) => {
      setMessages((prev) => [...prev, event.data]);
    };

    return () => {
      eventSource.close();
    };
  }, []);

  return (
    <ul>
      {messages.map((message, index) => (
        <li key={index}>{message}</li>
      ))}
    </ul>
  );
}

ブラウザで動かすとこんな感じ

まとめ

他のリアルタイム通信と比べると、実装負荷と通信量の部分でコスパがいいと思います。接続数に制限があるため大規模なアプリだと難しいですが、個人開発程度だと使い勝手の良さそうな通信方法だと思っています。
いつかこれを使ったアプリの記事書きたい。

株式会社ソニックムーブ

Discussion