<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Backend on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/categories/backend/</link>
    <description>Recent content in Backend on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Mon, 23 Feb 2026 18:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/categories/backend/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Supabaseで構築するスケーラブルなデータベース基盤</title>
      <link>https://www.ai2core.com/posts/2026-02-23-supabase-db/</link>
      <pubDate>Mon, 23 Feb 2026 18:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-23-supabase-db/</guid>
      <description>Firebase代替としてのSupabaseの魅力と、Postgresの強力な機能。</description>
      <content:encoded><![CDATA[<h1 id="supabaseで構築するスケーラブルなデータベース基盤">Supabaseで構築するスケーラブルなデータベース基盤</h1>
<h2 id="はじめに">はじめに</h2>
<p>「バックエンドの開発速度を上げたい」「認証やリアルタイム機能を手軽に実装したい」——こうした要求に応えるBaaS (Backend as a Service) は、現代のアプリケーション開発において不可欠な存在です。その代表格であるFirebaseは、多くのプロジェクトで採用され、開発者に多大な恩恵をもたらしてきました。</p>
<p>しかし、プロジェクトが成長し、データ構造が複雑化するにつれて、このような課題に直面したことはないでしょうか？</p>
<ul>
<li>「Firebase (Firestore) のスキーマレスな性質が、逆にデータ整合性の維持を難しくしている…」</li>
<li>「複雑なデータ検索や集計を行いたいが、NoSQLのクエリでは表現力に限界がある…」</li>
<li>「ベンダーロックインが心配だ。将来的にインフラを移行する必要が出たときに、身動きが取れなくなるのではないか？」</li>
<li>「リレーショナルなデータを扱うには、Firestoreは最適とは言えないかもしれない…」</li>
</ul>
<p>もし、あなたがこれらの課題に少しでも心当たりがあるなら、この記事はあなたのためのものです。</p>
<p>本記事では、<strong>「オープンソースのFirebase代替」<strong>として注目を集める</strong>Supabase</strong>を取り上げます。Supabaseは、単なるFirebaseのクローンではありません。その核には、40年以上の歴史と絶大な信頼性を誇るリレーショナルデータベース<strong>PostgreSQL</strong>が据えられています。</p>
<p>この記事を読み終える頃には、あなたはSupabaseがなぜスケーラブルで堅牢なデータベース基盤を構築するための強力な選択肢となるのか、そしてPostgreSQLの力を最大限に活用して、高速な開発と長期的な運用性を両立させる方法を深く理解できるでしょう。</p>
<h2 id="なぜsupabaseが今注目されているのか---背景と課題">なぜSupabaseが今、注目されているのか？ - 背景と課題</h2>
<p>Supabaseの魅力を理解するためには、まずBaaS市場の変遷と、既存のサービスが抱える課題を理解する必要があります。</p>
<h3 id="baasの進化とfirebaseがもたらした革命">BaaSの進化とFirebaseがもたらした革命</h3>
<p>かつて、Webアプリケーションを開発するには、サーバーのプロビジョニング、データベースのセットアップ、APIサーバーの実装、認証システムの構築など、多くの定型的な作業が必要でした。</p>
<p>BaaSは、これらのバックエンド機能を汎用的なサービスとして提供することで、開発者がフロントエンドやアプリケーションのコアロジックに集中できるようにしました。中でもGoogleのFirebaseは、直感的なAPI、リアルタイムデータベース、強力な認証機能、ホスティングまでをワンストップで提供し、特にモバイルアプリやプロトタイピングの領域で圧倒的な支持を得ました。</p>
<h3 id="firebase-firestore-が抱えるスケーラビリティの課題">Firebase (Firestore) が抱えるスケーラビリティの課題</h3>
<p>Firebaseの成功は、その手軽さと開発速度にありました。しかし、プロジェクトが成長し、エンタープライズレベルの要件が求められるようになると、そのアーキテクチャに起因するいくつかの課題が顕在化します。</p>
<ol>
<li>
<p><strong>NoSQLデータベースの限界</strong>:
Firebaseの主要なデータベースであるFirestoreは、ドキュメント指向のNoSQLデータベースです。スキーマレスであるため初期開発は迅速ですが、データ間の複雑なリレーションを扱うのが苦手です。例えば、SNSアプリケーションで「ユーザー」と「投稿」と「コメント」と「いいね」が複雑に絡み合うようなデータモデルを考えたとき、正規化されたリレーショナルデータベースであればJOIN一発で取得できるデータも、Firestoreでは複数回のクエリやデータの非正規化といった工夫が必要になり、コードの複雑化やデータ冗長性を招きます。</p>
</li>
<li>
<p><strong>クエリの表現力不足</strong>:
SQLのように柔軟で強力なクエリ言語を持たないため、複雑な条件での絞り込み、集計、ソートといった操作に制限があります。<code>GROUP BY</code>や<code>HAVING</code>のような集計関数を使いたい場合、Cloud Functionsなどを駆使して自前で実装する必要があり、リアルタイム性やパフォーマンスが犠牲になることも少なくありません。</p>
</li>
<li>
<p><strong>ベンダーロックインへの懸念</strong>:
Firebaseは非常に優れたエコシステムですが、それはGoogle Cloud Platformに深く統合されています。一度Firebaseで大規模なシステムを構築すると、データベースの移行や、他のクラウドサービスとの連携が困難になる「ベンダーロックイン」のリスクが常に伴います。データのエクスポートは可能ですが、セキュリティルールやCloud Functionsで記述したビジネスロジックまで含めた完全な移行は、極めて困難です。</p>
</li>
</ol>
<p>これらの課題は、「開発の初期段階では最高のツールだが、長期的にスケールさせるには不安が残る」という評価につながっていました。</p>
<h3 id="rdbへの回帰とsupabaseの登場">RDBへの回帰とSupabaseの登場</h3>
<p>このような背景の中、開発者コミュニティでは、データの整合性、トランザクションの信頼性、そしてSQLという標準化された強力なクエリ言語を持つ<strong>リレーショナルデータベース (RDB) の価値</strong>が再評価されるようになります。</p>
<p>そこに登場したのがSupabaseです。Supabaseは、この流れを見事に捉えました。</p>
<p><strong>「世界で最も信頼されているオープンソースRDBであるPostgreSQLを使い、Firebaseのような開発者体験を提供する」</strong></p>
<p>このコンセプトが、多くの開発者の心を掴んだのです。Supabaseは、BaaSの手軽さと、RDBの堅牢性・柔軟性という、これまでトレードオフの関係にあると考えられていた2つの要素を、見事に両立させました。</p>
<h2 id="supabaseのアーキテクチャとpostgresqlの強力な機能">SupabaseのアーキテクチャとPostgreSQLの強力な機能</h2>
<p>Supabaseが単なるデータベースサービスではないことを理解するために、そのアーキテクチャを見ていきましょう。Supabaseは、既存の優れたオープンソースツール群をPostgreSQLを中心に統合した、いわば「バックエンドのオーケストラ」です。</p>
<pre tabindex="0"><code>                    +--------------------------------+
                    |       Your Application         |
                    | (Web, Mobile, etc.)            |
                    +--------------------------------+
                           |         |         |
                           | (SDK)   | (SDK)   |
  +------------------------+---------+---------+--------------------------+
  |                   Supabase Platform (Hosted or Self-hosted)             |
  |                                                                         |
  |  +-----------+   +-------------+   +-----------+   +---------+   +----------+
  |  |  Auth     |   | Realtime    |   |  Storage  |   | Edge    |   | REST API |
  |  | (GoTrue)  |   | (Realtime)  |   | (S3-comp) |   | Functions| |(PostgREST)|
  |  +-----------+   +-------------+   +-----------+   +---------+   +----------+
  |        |                 |               |               |           |
  |        +-----------------+---------------+---------------+-----------+
  |                                    |
  |                          +---------------------+
  |                          |     PostgreSQL      |  &lt;-- THE CORE
  |                          | (Database, RLS,     |
  |                          |  Functions, Exts)   |
  |                          +---------------------+
  +-------------------------------------------------------------------------+
</code></pre><ul>
<li><strong>PostgreSQL</strong>: すべての中心です。単なるデータストアではなく、認証情報、セキュリティポリシー、ビジネスロジック（関数）まで、すべてがここに集約されます。</li>
<li><strong>GoTrue</strong>: JWTベースの認証サーバー。ユーザー管理とアクセストークン発行を担当します。ユーザー情報はPostgresの<code>auth.users</code>テーブルに保存されます。</li>
<li><strong>PostgREST</strong>: データベーススキーマを読み取り、自動的にRESTful APIを生成します。テーブルやビューを作成するだけで、即座に対応するAPIエンドポイントが利用可能になります。</li>
<li><strong>Realtime</strong>: Postgresの論理レプリケーション機能を利用して、データベースの変更をリアルタイムにクライアントにWebSocket経由で配信します。</li>
<li><strong>Storage</strong>: S3互換のオブジェクトストレージ。Postgresを使って権限管理を行います。</li>
<li><strong>Edge Functions</strong>: Denoで書かれたサーバーレス関数。データベースに近い場所でカスタムロジックを実行できます。</li>
</ul>
<p>このアーキテクチャの最大のポイントは、<strong>すべてがPostgreSQLに根ざしている</strong>ことです。これにより、PostgreSQLが持つ強力な機能を最大限に活用できるのです。</p>
<h3 id="postgresqlがもたらすスケーラビリティと柔軟性">PostgreSQLがもたらすスケーラビリティと柔軟性</h3>
<p>SupabaseがFirebaseと一線を画すのは、このPostgreSQLの力です。具体的にどのようなメリットがあるのか見ていきましょう。</p>
<h4 id="1-厳密なスキーマとリレーション">1. 厳密なスキーマとリレーション</h4>
<p>NoSQLの柔軟性も魅力ですが、大規模なアプリケーションでは厳密なスキーマがデータの整合性を保証し、バグの温床を減らします。Supabaseでは、使い慣れたSQLでテーブルを定義できます。</p>
<p><strong>例: ユーザーと投稿テーブルの作成</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- ユーザーテーブル
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">public</span>.profiles (
</span></span><span style="display:flex;"><span>  id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">REFERENCES</span> auth.users(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
</span></span><span style="display:flex;"><span>  username TEXT <span style="color:#66d9ef">UNIQUE</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>  updated_at TIMESTAMPTZ <span style="color:#66d9ef">DEFAULT</span> NOW()
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- 投稿テーブル
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">public</span>.posts (
</span></span><span style="display:flex;"><span>  id BIGINT <span style="color:#66d9ef">GENERATED</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#66d9ef">AS</span> <span style="color:#66d9ef">IDENTITY</span> <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span>,
</span></span><span style="display:flex;"><span>  user_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> <span style="color:#66d9ef">public</span>.profiles(id),
</span></span><span style="display:flex;"><span>  content TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>  created_at TIMESTAMPTZ <span style="color:#66d9ef">DEFAULT</span> NOW()
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>このように、<code>FOREIGN KEY</code>制約を使えば、存在しないユーザーからの投稿を防ぐなど、データベースレベルでデータの整合性を担保できます。</p>
<p>そして、リレーショナルデータベースの真骨頂である<code>JOIN</code>が、その威力を発揮します。</p>
<p><strong>例: 投稿とその投稿者のユーザー名を取得する (JavaScript SDK)</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createClient</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@supabase/supabase-js&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">supabase</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createClient</span>(<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">SUPABASE_URL</span>, <span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">SUPABASE_ANON_KEY</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">fetchPostsWithUsernames</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">error</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">supabase</span>
</span></span><span style="display:flex;"><span>    .<span style="color:#a6e22e">from</span>(<span style="color:#e6db74">&#39;posts&#39;</span>)
</span></span><span style="display:flex;"><span>    .<span style="color:#a6e22e">select</span>(<span style="color:#e6db74">`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      id,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      content,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      created_at,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      profiles (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        username
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    `</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) <span style="color:#66d9ef">throw</span> <span style="color:#a6e22e">error</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">data</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// 出力例:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">//   {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">//     id: 1,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">//     content: &#39;Hello Supabase!&#39;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">//     created_at: &#39;...&#39;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">//     profiles: { username: &#39;user1&#39; }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">//   }, ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// ]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>Supabaseのクライアントライブラリは、リレーションを直感的に扱えるように設計されており、<code>JOIN</code>の強力な機能を簡単に利用できます。Firestoreでこれと同じことをしようとすると、クライアント側で複数回の読み取りが必要になるケースがほとんどです。</p>
<h4 id="2-強力なセキュリティ-row-level-security-rls">2. 強力なセキュリティ: Row Level Security (RLS)</h4>
<p>Supabaseのキラー機能の一つが、PostgreSQLの**Row Level Security (RLS)**です。これは、データベースの行（レコード）単位で、誰がどの操作（SELECT, INSERT, UPDATE, DELETE）を行えるかを定義できるセキュリティ機能です。</p>
<p>FirebaseのセキュリティルールがJSONライクな独自構文で記述するのに対し、RLSは<strong>SQL</strong>でポリシーを記述します。これにより、極めて柔軟かつ強力なアクセスコントロールが実現できます。</p>
<p><strong>例1: 自分のプロフィール情報しか更新できないようにするポリシー</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- まずテーブルでRLSを有効化
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">public</span>.profiles ENABLE <span style="color:#66d9ef">ROW</span> <span style="color:#66d9ef">LEVEL</span> <span style="color:#66d9ef">SECURITY</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- UPDATEに対するポリシーを作成
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> POLICY <span style="color:#e6db74">&#34;Users can update their own profile.&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">public</span>.profiles
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FOR</span> <span style="color:#66d9ef">UPDATE</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">USING</span> ( auth.uid() <span style="color:#f92672">=</span> id ); <span style="color:#75715e">-- 現在認証中のユーザーのIDと、行のIDが一致する場合のみ許可
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><strong>例2: ログインしているユーザーは全ての投稿を閲覧でき、自分の投稿のみ削除できるポリシー</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">public</span>.posts ENABLE <span style="color:#66d9ef">ROW</span> <span style="color:#66d9ef">LEVEL</span> <span style="color:#66d9ef">SECURITY</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- ログインユーザーは全件閲覧可能
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> POLICY <span style="color:#e6db74">&#34;Allow logged-in users to read all posts.&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">public</span>.posts
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FOR</span> <span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">USING</span> ( auth.<span style="color:#66d9ef">role</span>() <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;authenticated&#39;</span> );
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- 自分の投稿のみ削除可能
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> POLICY <span style="color:#e6db74">&#34;Allow users to delete their own posts.&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">public</span>.posts
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FOR</span> <span style="color:#66d9ef">DELETE</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">USING</span> ( auth.uid() <span style="color:#f92672">=</span> user_id );
</span></span></code></pre></td></tr></table>
</div>
</div><p>これらのポリシーはデータベース層で強制されるため、クライアントからどのようなリクエストが来ても、不正な操作はブロックされます。PostgRESTが生成するAPIは、このRLSポリシーを自動的に尊重するため、APIサーバー側で複雑な権限チェックロジックを実装する必要がほとんどありません。</p>
<h4 id="3-トランザクションとビジネスロジック">3. トランザクションとビジネスロジック</h4>
<p>複数のデータ更新を伴う処理、例えば「銀行振込（A口座から減算し、B口座に加算する）」のような処理では、全ての操作が成功するか、全て失敗するかのどちらかでなければなりません。これを保証するのが<strong>トランザクション</strong>です。</p>
<p>PostgreSQLは、ACID特性に準拠した強力なトランザクション機能を備えています。Supabaseでは、PostgreSQLの関数を<code>rpc()</code>（Remote Procedure Call）として呼び出すことで、トランザクションを安全に実行できます。</p>
<p><strong>例: 投稿に「いいね」をする関数（重複いいねを防ぐ）</strong>
まず、PostgreSQLに関数を作成します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- いいねテーブル
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">public</span>.likes (
</span></span><span style="display:flex;"><span>  post_id BIGINT <span style="color:#66d9ef">REFERENCES</span> <span style="color:#66d9ef">public</span>.posts(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
</span></span><span style="display:flex;"><span>  user_id UUID <span style="color:#66d9ef">REFERENCES</span> <span style="color:#66d9ef">public</span>.profiles(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> (post_id, user_id)
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- いいねを追加する関数
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">OR</span> <span style="color:#66d9ef">REPLACE</span> <span style="color:#66d9ef">FUNCTION</span> like_post (post_id_to_like BIGINT)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">RETURNS</span> void
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LANGUAGE</span> plpgsql
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AS</span> <span style="color:#960050;background-color:#1e0010">$$</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">-- すでにいいねしている場合は何もしない
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">IF</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> (<span style="color:#66d9ef">SELECT</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">FROM</span> <span style="color:#66d9ef">public</span>.likes <span style="color:#66d9ef">WHERE</span> post_id <span style="color:#f92672">=</span> post_id_to_like <span style="color:#66d9ef">AND</span> user_id <span style="color:#f92672">=</span> auth.uid()) <span style="color:#66d9ef">THEN</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">public</span>.likes (post_id, user_id)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">VALUES</span> (post_id_to_like, auth.uid());
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">END</span> <span style="color:#66d9ef">IF</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">END</span>;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">$$</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この関数は、<code>likes</code>テーブルへの<code>INSERT</code>を試みますが、主キー制約により同じユーザーが同じ投稿に複数回いいねすることはできません。<code>IF</code>文で事前にチェックすることも可能です。</p>
<p>クライアントからは、この関数をシンプルに呼び出すだけです。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">likePost</span>(<span style="color:#a6e22e">postId</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">error</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">supabase</span>.<span style="color:#a6e22e">rpc</span>(<span style="color:#e6db74">&#39;like_post&#39;</span>, {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">post_id_to_like</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">postId</span>
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">&#39;Error liking post:&#39;</span>, <span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Successfully liked post!&#39;</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このように、複雑なビジネスロジックやアトミックな操作が必要な処理をデータベース関数としてカプセル化することで、クライアント側のコードはシンプルになり、セキュリティとデータ整合性も向上します。</p>
<h4 id="4-無限の拡張性-postgresql-extensions">4. 無限の拡張性: PostgreSQL Extensions</h4>
<p>PostgreSQLのもう一つの強みは、その広大なエコシステムと拡張機能（Extensions）です。Supabaseでは、ダッシュボードからワンクリックで様々な拡張機能を有効にできます。</p>
<ul>
<li><strong>PostGIS</strong>: 地理空間データを扱うためのデファクトスタンダード。位置情報を使った検索や分析がSQLで可能になります。</li>
<li><strong>pg_cron</strong>: 定期的なジョブ実行をデータベース内でスケジューリングできます（例: 毎晩0時に古いログを削除する）。</li>
<li><strong>pgvector</strong>: ベクトルデータを効率的に保存・検索するための拡張機能。AI/MLアプリケーションにおける類似検索（画像検索、推薦システムなど）に不可欠です。</li>
<li><strong>TimescaleDB</strong>: 時系列データを高速に処理するための拡張機能。IoTデータや金融データの分析基盤として利用できます。</li>
</ul>
<p>これらはほんの一例です。FirebaseではCloud Functionsなどを駆使して外部サービスと連携する必要があるような機能も、SupabaseならPostgreSQLの拡張機能として、データベースと一体化した形で実現できる可能性があります。</p>
<h2 id="supabase-vs-firebase-メリットとデメリットの徹底比較">Supabase vs Firebase: メリットとデメリットの徹底比較</h2>
<p>ここで、両者を客観的に比較し、それぞれのツールの強みと弱みを整理してみましょう。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">項目</th>
          <th style="text-align: left">Supabase</th>
          <th style="text-align: left">Firebase</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>データベース</strong></td>
          <td style="text-align: left">PostgreSQL (リレーショナル)</td>
          <td style="text-align: left">Firestore (NoSQL), Realtime DB</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>スキーマ</strong></td>
          <td style="text-align: left">スキーマあり（厳密）</td>
          <td style="text-align: left">スキーマレス（柔軟）</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>クエリ</strong></td>
          <td style="text-align: left">SQL（JOIN, 集計など自由自在）</td>
          <td style="text-align: left">独自クエリ（制限あり、JOIN不可）</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ベンダーロックイン</strong></td>
          <td style="text-align: left"><strong>低い</strong>（オープンソース, セルフホスト可）</td>
          <td style="text-align: left"><strong>高い</strong>（Google Cloudに依存）</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>セキュリティ</strong></td>
          <td style="text-align: left"><strong>RLS</strong> (SQLベース、行単位で強力)</td>
          <td style="text-align: left">Security Rules (JSONライク、パスベース)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ビジネスロジック</strong></td>
          <td style="text-align: left">DB関数 (RPC), Edge Functions</td>
          <td style="text-align: left">Cloud Functions</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>拡張性</strong></td>
          <td style="text-align: left"><strong>Postgres Extensions</strong></td>
          <td style="text-align: left">Google Cloudサービス連携</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ローカル開発</strong></td>
          <td style="text-align: left">Dockerで完全な環境を再現可能</td>
          <td style="text-align: left">エミュレータスイート</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>学習コスト</strong></td>
          <td style="text-align: left">SQL/RDBの知識が必要</td>
          <td style="text-align: left">比較的容易に始められる</td>
      </tr>
  </tbody>
</table>
<h3 id="supabaseのメリット">Supabaseのメリット</h3>
<ul>
<li><strong>データ整合性と信頼性</strong>: RDBの特性により、データの整合性を担保しやすい。</li>
<li><strong>クエリの表現力</strong>: SQLが使えるため、複雑なデータ取得や分析が容易。</li>
<li><strong>脱ベンダーロックイン</strong>: オープンソースであり、<code>pg_dump</code>一発でデータをエクスポート可能。最悪の場合、自分でPostgreSQLをホストすることもできます。</li>
<li><strong>PostgreSQLエコシステムの活用</strong>: 長年培われてきたPostgreSQLのツール、知見、拡張機能という巨人の肩の上に立つことができます。</li>
<li><strong>コスト効率</strong>: 複雑なクエリをDB層で実行できるため、Cloud Functionsのように読み書きの回数で課金が増大するのを避けられる可能性があります。</li>
</ul>
<h3 id="supabaseのデメリット注意点">Supabaseのデメリット・注意点</h3>
<ul>
<li><strong>RDBの知識</strong>: スキーマ設計、正規化、インデックスの知識など、RDBに関する基本的な理解が求められます。</li>
<li><strong>スケーリングの考え方</strong>: Firestoreは水平スケーリングを前提に設計されていますが、PostgreSQLのスケールは基本的には垂直スケーリング（サーバーのスペックアップ）が中心となります。もちろん、リードレプリカなどでスケールアウトも可能ですが、Firestoreとは思想が異なります。ただし、ほとんどのアプリケーションにとってPostgreSQLのパフォーマンスは十分すぎるほど高性能です。</li>
<li><strong>エコシステムの成熟度</strong>: Firebaseに比べると、コミュニティの規模やサードパーティ製のライブラリ、学習資料などはまだ発展途上な面もあります（しかし、急速に成長しています）。</li>
</ul>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>Supabaseをプロダクションで活用するための、より実践的なヒントをいくつか紹介します。</p>
<h3 id="1-データベースマイグレーションはcliで管理する">1. データベースマイグレーションはCLIで管理する</h3>
<p>Supabaseのダッシュボード上でGUIでテーブルを操作するのは手軽ですが、プロダクション環境ではスキーマの変更履歴をバージョン管理することが不可欠です。Supabaseは強力なCLIツールを提供しています。</p>
<p><strong>基本的なマイグレーションフロー:</strong></p>
<ol>
<li>
<p><strong>ローカル環境と連携:</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>supabase login
</span></span><span style="display:flex;"><span>supabase link --project-ref &lt;your-project-id&gt;
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>ローカルDBのスキーマ変更をダンプ:</strong>
リモート（本番）DBのスキーマ変更をローカルに反映します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>supabase db pull
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>ローカルでテーブル変更などを行い、差分からマイグレーションファイルを作成:</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 例: postsテーブルに &#34;title&#34; カラムを追加したとする</span>
</span></span><span style="display:flex;"><span>supabase db diff -f add_title_to_posts
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、<code>supabase/migrations</code>ディレクトリにSQLファイルが生成されます。</p>
</li>
<li>
<p><strong>マイグレーションをリモートDBに適用:</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>supabase db push
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ol>
<p>このフローをCI/CDパイプラインに組み込むことで、データベーススキーマの変更を安全かつ自動的に管理できます。</p>
<h3 id="2-パフォーマンスチューニングの勘所-インデックスと実行計画">2. パフォーマンスチューニングの勘所: インデックスと実行計画</h3>
<p>アプリケーションのパフォーマンスが低下してきたら、まずクエリを疑います。</p>
<ul>
<li>
<p><strong>インデックスの作成</strong>: <code>WHERE</code>句で頻繁に検索するカラムや、<code>JOIN</code>の結合キーとなるカラムにはインデックスを作成しましょう。Supabaseのダッシュボードには、遅いクエリを検出し、インデックス作成を推奨してくれる機能もあります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- postsテーブルのuser_idカラムにインデックスを作成
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_posts_on_user_id <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">public</span>.posts(user_id);
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>実行計画の確認</strong>: <code>EXPLAIN ANALYZE</code>を使うと、PostgreSQLがどのようにクエリを実行しているか（実行計画）を確認できます。<code>Seq Scan</code>（全件スキャン）が発生している箇所は、インデックスが効いていない可能性があり、チューニングの対象となります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">EXPLAIN</span> <span style="color:#66d9ef">ANALYZE</span> <span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> <span style="color:#66d9ef">public</span>.posts <span style="color:#66d9ef">WHERE</span> user_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;...&#39;</span>;
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
<h3 id="3-ビュー-view-を活用してapiをシンプルに保つ">3. ビュー (VIEW) を活用してAPIをシンプルに保つ</h3>
<p>複数のテーブルを<code>JOIN</code>した複雑なデータ構造を頻繁にクライアントから要求される場合、<strong>ビュー</strong>を作成するのが効果的です。ビューは、保存されたクエリ結果を仮想的なテーブルとして扱える機能です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">VIEW</span> <span style="color:#66d9ef">public</span>.posts_with_username <span style="color:#66d9ef">AS</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  p.id,
</span></span><span style="display:flex;"><span>  p.content,
</span></span><span style="display:flex;"><span>  p.created_at,
</span></span><span style="display:flex;"><span>  u.username
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span>.posts p
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span>.profiles u <span style="color:#66d9ef">ON</span> p.user_id <span style="color:#f92672">=</span> u.id;
</span></span></code></pre></td></tr></table>
</div>
</div><p>こうすることで、クライアントからはあたかも<code>posts_with_username</code>という単一のテーブルがあるかのように見え、シンプルなクエリでデータを取得できます。RLSポリシーもビューに対して設定可能です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// クライアント側のコードがシンプルになる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">error</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">supabase</span>
</span></span><span style="display:flex;"><span>  .<span style="color:#a6e22e">from</span>(<span style="color:#e6db74">&#39;posts_with_username&#39;</span>)
</span></span><span style="display:flex;"><span>  .<span style="color:#a6e22e">select</span>(<span style="color:#e6db74">&#39;*&#39;</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="4-security-definer-関数で権限昇格を安全に行う">4. <code>security definer</code> 関数で権限昇格を安全に行う</h3>
<p>通常、<code>rpc</code>で呼び出される関数は、実行したユーザーの権限で動作します（<code>security invoker</code>）。しかし、時には関数の定義者（通常は管理者）の権限で特定の操作を行いたい場合があります。例えば、「ユーザー登録時に、プロフィールテーブルにもレコードを自動で作成する」といったケースです。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- `auth.users`テーブルに新しい行が挿入されるたびにトリガーされる関数
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">OR</span> <span style="color:#66d9ef">REPLACE</span> <span style="color:#66d9ef">FUNCTION</span> <span style="color:#66d9ef">public</span>.handle_new_user()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">RETURNS</span> <span style="color:#66d9ef">TRIGGER</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LANGUAGE</span> plpgsql
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SECURITY</span> <span style="color:#66d9ef">DEFINER</span> <span style="color:#75715e">-- この関数を定義者の権限で実行する
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SET</span> search_path <span style="color:#f92672">=</span> <span style="color:#66d9ef">public</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AS</span> <span style="color:#960050;background-color:#1e0010">$$</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">public</span>.profiles (id, username)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">VALUES</span> (<span style="color:#66d9ef">new</span>.id, <span style="color:#66d9ef">new</span>.raw_user_meta_data<span style="color:#f92672">-&gt;&gt;</span><span style="color:#e6db74">&#39;username&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">RETURN</span> <span style="color:#66d9ef">new</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">END</span>;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">$$</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- `auth.users`テーブルへのINSERTをトリガーにする
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TRIGGER</span> on_auth_user_created
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AFTER</span> <span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">ON</span> auth.users
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">FOR</span> <span style="color:#66d9ef">EACH</span> <span style="color:#66d9ef">ROW</span> <span style="color:#66d9ef">EXECUTE</span> <span style="color:#66d9ef">PROCEDURE</span> <span style="color:#66d9ef">public</span>.handle_new_user();
</span></span></code></pre></td></tr></table>
</div>
</div><p>この<code>SECURITY DEFINER</code>を使うと、ユーザーは<code>profiles</code>テーブルへの直接の<code>INSERT</code>権限を持っていなくても、サインアップするだけで自動的にプロフィールが作成される、という挙動を実現できます。ただし、強力な機能であるため、SQLインジェクションなどの脆弱性を作り込まないよう細心の注意が必要です。</p>
<h2 id="まとめ">まとめ</h2>
<p>本記事では、Supabaseが単なる「Firebaseの代替」ではなく、<strong>信頼と実績のあるPostgreSQLを核とした、スケーラブルで堅牢なデータベース基盤</strong>であることを、そのアーキテクチャと具体的な機能を通して解説しました。</p>
<ul>
<li><strong>BaaSの手軽さ</strong>: 認証、リアルタイム、ストレージといったバックエンド機能をすぐに利用開始できます。</li>
<li><strong>RDBの堅牢性</strong>: スキーマ、リレーション、トランザクションにより、データの整合性を高いレベルで保証します。</li>
<li><strong>SQLの表現力</strong>: 複雑なデータ取得や分析も、強力で標準化されたSQLで実現できます。</li>
<li><strong>強力なセキュリティ</strong>: Row Level Security (RLS)により、データベース層で行レベルのきめ細やかなアクセスコントロールが可能です。</li>
<li><strong>オープンソース</strong>: ベンダーロックインのリスクが低く、PostgreSQLの広大なエコシステムを最大限に活用できます。</li>
</ul>
<p><strong>どのようなプロジェクトにSupabaseは向いているでしょうか？</strong></p>
<ul>
<li><strong>データ整合性が最重要</strong>なアプリケーション（金融、業務システムなど）</li>
<li><strong>複雑なクエリやデータ分析</strong>が必要なサービス（SaaS、分析ツールなど）</li>
<li><strong>長期的な運用とスケーラビリティ</strong>を見据えたプロジェクト</li>
<li>開発チームが<strong>SQLとRDBに慣れ親しんでいる</strong>場合</li>
</ul>
<p>一方で、スキーマが固まらない超初期のプロトタイピングや、とにかく高速にシンプルなアプリを立ち上げたい場合には、依然としてFirebaseのスキーマレスなアプローチに分があるかもしれません。</p>
<p>しかし、今日のアプリケーション開発において、データの価値はますます高まっています。その大切なデータを、場当たり的な設計ではなく、堅牢な基盤の上で長期的に育てていきたいと考えるならば、Supabaseは検討すべき非常に有力な選択肢です。</p>
<p>Supabaseは、私たち開発者に「BaaSの利便性」と「本格的なデータベース管理」の二者択一を迫るのではなく、その両方を手に入れる道を示してくれました。ぜひ、次のプロジェクトでこのパワフルなデータベース基盤を体験してみてください。</p>
]]></content:encoded>
      <category>Backend</category>
      <category>Supabase</category>
      <category>PostgreSQL</category>
      <category>Database</category>
    </item>
    <item>
      <title>GraphQL vs tRPC：2026年のAPI選定基準</title>
      <link>https://www.ai2core.com/posts/2026-02-23-graphql-trpc/</link>
      <pubDate>Mon, 23 Feb 2026 12:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-23-graphql-trpc/</guid>
      <description>フルスタックTypeScript開発におけるtRPCの優位性と、GraphQLの使い所。</description>
      <content:encoded><![CDATA[<h1 id="graphql-vs-trpc2026年のapi選定基準">GraphQL vs tRPC：2026年のAPI選定基準</h1>
<h2 id="はじめに">はじめに</h2>
<p>「新しいWebアプリケーションの技術選定、APIはどうしよう？」
「RESTはもう古いと聞くけど、GraphQLを学ぶべきだろうか？」
「最近、界隈でtRPCという技術が絶賛されているけど、GraphQLと何が違うんだろう？」
「そもそも、&ldquo;End-to-Endで型安全&quot;って、具体的にどんなメリットがあるの？」</p>
<p>もしあなたがフルスタックTypeScript開発者で、このような疑問を一度でも抱いたことがあるなら、この記事はまさにあなたのために書かれました。</p>
<p>現代のWeb開発において、APIはアプリケーションの心臓部です。フロントエンドとバックエンドを繋ぐこの重要なコンポーネントの設計は、開発体験、生産性、そしてアプリケーションの品質そのものを大きく左右します。</p>
<p>かつてはRESTがAPIのデファクトスタンダードでしたが、アプリケーションが複雑化するにつれて、その限界も明らかになってきました。そこに登場したのが、柔軟なデータ取得を可能にするGraphQLです。そして今、TypeScriptの普及を背景に、究極のシンプルさと型安全性を追求したtRPCが急速に注目を集めています。</p>
<p>この記事では、2026年を見据えた未来のAPI開発という視点から、GraphQLとtRPCを徹底的に比較・解説します。両者の思想、具体的な実装方法、メリット・デメリットを深く掘り下げ、あなたの次のプロジェクトにどちらの技術が最適なのか、明確な判断基準を提供します。この記事を読み終える頃には、あなたは自信を持って最適なAPI技術を選定できるエンジニアになっているはずです。</p>
<h2 id="api技術の進化と現代の課題">API技術の進化と現代の課題</h2>
<p>なぜ今、GraphQLやtRPCが注目されているのでしょうか？その背景を理解するために、まずはAPI技術が歩んできた道のりと、そこで浮き彫りになった課題を振り返ってみましょう。</p>
<h3 id="restの時代とその功罪">RESTの時代と、その功罪</h3>
<p>長年にわたり、REST (REpresentational State Transfer) はWeb APIの設計原則として広く採用されてきました。HTTPメソッド（GET, POST, PUT, DELETE）とURLでリソースを表現するそのシンプルさと、HTTPとの親和性の高さが支持された理由です。</p>
<p>しかし、フロントエンドがリッチでインタラクティブになるにつれて、RESTのいくつかの課題が顕在化しました。</p>
<ol>
<li><strong>Over-fetching（過剰なデータ取得）</strong>: ある画面ではユーザーの名前だけが必要なのに、<code>/users/:id</code> エンドポイントが常に住所や電話番号まで返してしまう問題。不要なデータを転送するため、特にモバイル環境ではパフォーマンスに影響します。</li>
<li><strong>Under-fetching（不十分なデータ取得）</strong>: ユーザー情報とその最新の投稿一覧を表示したい場合、まず <code>/users/:id</code> を叩き、次に <code>/users/:id/posts</code> を叩く、というように複数のリクエストが必要になる問題。俗に言う「N+1問題」の温床であり、画面表示の遅延に繋がります。</li>
<li><strong>エンドポイントの乱立</strong>: 機能追加のたびに新しいエンドポイントが増え、<code>/v1</code>, <code>/v2</code> のようなバージョン管理や、Swagger/OpenAPIといった仕様書のメンテナンスコストが増大します。</li>
<li><strong>フロントとバックの連携コスト</strong>: APIの仕様変更があった際、フロントエンドとバックエンドで型の不整合が起きやすく、実行時エラーの原因となります。この「口約束」になりがちな連携が、開発のボトルネックになることは少なくありません。</li>
</ol>
<h3 id="救世主としてのgraphql">救世主としてのGraphQL</h3>
<p>これらのRESTの問題を解決するために登場したのがGraphQLです。Facebook（現Meta）によって開発されたGraphQLは、APIのための「クエリ言語」です。</p>
<p>GraphQLは、RESTとは全く異なるアプローチでAPI通信に革命をもたらしました。</p>
<ul>
<li><strong>クライアント主導のデータ取得</strong>: フロントエンドが必要なデータの構造を「クエリ」として送信し、サーバーはまさにその通りの構造でデータを返します。これにより、Over-fetchingとUnder-fetchingが根本的に解決されます。</li>
<li><strong>単一エンドポイント</strong>: <code>/graphql</code> のような単一のエンドポイントに対して、すべてのリクエストをPOSTメソッドで送信します。エンドポイントの乱立やバージョン管理の問題から解放されます。</li>
<li><strong>強力な型システム</strong>: APIの仕様は「スキーマ」として厳密に定義されます。このスキーマは、APIのドキュメントとして機能するだけでなく、開発ツールによる強力なサポート（入力補完やバリデーション）を可能にします。</li>
</ul>
<p>GraphQLは、特に多種多様なクライアント（Web、iOS、Androidなど）を持つ大規模なアプリケーションや、マイクロサービスアーキテクチャにおいて、その真価を発揮しました。</p>
<h3 id="typescriptの台頭と究極の型安全性への渇望">TypeScriptの台頭と「究極の型安全性」への渇望</h3>
<p>そして、もう一つの大きな潮流がTypeScriptの普及です。フロントエンドからバックエンド（Node.js）まで、JavaScriptが使われるあらゆる場所でTypeScriptが採用され、「フルスタックTypeScript」という開発スタイルが現実のものとなりました。</p>
<p>これにより、開発者は新たな高みを求めるようになります。それは、**「アプリケーションの端から端まで（End-to-End）を一つの型システムで貫き、静的解析の恩恵を最大限に享受したい」**という願いです。</p>
<p>GraphQLはスキーマによって型安全性を提供しますが、それを実現するには <code>graphql-codegen</code> のようなツールを使い、スキーマからTypeScriptの型を「生成」するビルドステップが必要でした。この設定は時に複雑で、フロントエンドとバックエンドが密結合しているプロジェクトでは、少々大げさに感じられることもありました。</p>
<p>そこに彗星の如く現れたのがtRPCです。</p>
<p>tRPCは、「そもそもフロントとバックが両方TypeScriptなら、<strong>APIスキーマを別途定義する必要はない。バックエンドのTypeScriptの型定義そのものをスキーマとして共有すれば良いじゃないか</strong>」という、シンプルかつ画期的な発想に基づいています。</p>
<p>コード生成も、ビルドステップも不要。サーバーサイドのルーター定義から、クライアントサイドのAPI呼び出しの型が自動的に推論される。この圧倒的な開発体験が、フルスタックTypeScript開発者の心を鷲掴みにしたのです。</p>
<p>このように、API技術は「より柔軟に、より安全に、より開発体験を良くする」という方向へ進化を続けています。その最前線にいるGraphQLとtRPC、それぞれの具体的な実装と特性を詳しく見ていきましょう。</p>
<h2 id="詳細解説graphqlとtrpcの実装比較">詳細解説：GraphQLとtRPCの実装比較</h2>
<p>百聞は一見に如かず。ここでは、Next.js環境を想定し、ユーザー情報を取得・作成する簡単なAPIをGraphQLとtRPCそれぞれで実装してみます。</p>
<h3 id="graphqlスキーマ駆動開発の世界">GraphQL：スキーマ駆動開発の世界</h3>
<p>GraphQL開発は、まずAPIの仕様である「スキーマ」を定義することから始まります。</p>
<h4 id="1-図解graphqlの処理フロー">1. 図解：GraphQLの処理フロー</h4>
<p><em>上図のように、クライアントからのクエリは単一エンドポイントで受け付けられ、リゾルバが対応する処理を実行し、データを返します。</em></p>
<h4 id="2-サーバーサイド実装-with-apollo-server">2. サーバーサイド実装 (with Apollo Server)</h4>
<p>まずは必要なライブラリをインストールします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install @apollo/server graphql
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に、APIの仕様をスキーマ定義言語（SDL）で記述します。</p>
<p><strong><code>schema.graphql</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-graphql" data-lang="graphql"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">User</span> {
</span></span><span style="display:flex;"><span>  id: <span style="color:#a6e22e">ID</span>!
</span></span><span style="display:flex;"><span>  name: <span style="color:#a6e22e">String</span>!
</span></span><span style="display:flex;"><span>  email: <span style="color:#a6e22e">String</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Query</span> {
</span></span><span style="display:flex;"><span>  getUser(id: <span style="color:#a6e22e">ID</span>!): <span style="color:#a6e22e">User</span>
</span></span><span style="display:flex;"><span>  listUsers: [<span style="color:#a6e22e">User</span>!]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Mutation</span> {
</span></span><span style="display:flex;"><span>  createUser(name: <span style="color:#a6e22e">String</span>!, email: <span style="color:#a6e22e">String</span>): <span style="color:#a6e22e">User</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このスキーマに基づいて、実際のデータ操作を行う「リゾルバ」を実装します。</p>
<p><strong><code>server/resolvers.ts</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// サンプルのため、DBの代わりにインメモリ配列を使用
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">users</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1&#39;</span>, <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Alice&#39;</span>, <span style="color:#a6e22e">email</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;alice@example.com&#39;</span> },
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2&#39;</span>, <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Bob&#39;</span>, <span style="color:#a6e22e">email</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;bob@example.com&#39;</span> },
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">resolvers</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Query</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">getUser</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">_</span>: <span style="color:#66d9ef">any</span>, { <span style="color:#a6e22e">id</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span> }) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">users</span>.<span style="color:#a6e22e">find</span>(<span style="color:#a6e22e">user</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">id</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">id</span>) <span style="color:#f92672">||</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">listUsers</span><span style="color:#f92672">:</span> () <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">users</span>,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Mutation</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">createUser</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">_</span>: <span style="color:#66d9ef">any</span>, { <span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">email</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">email?</span>: <span style="color:#66d9ef">string</span> }) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">newUser</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">String</span>(<span style="color:#a6e22e">users</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>), <span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">email</span> };
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">users</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">newUser</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">newUser</span>;
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></td></tr></table>
</div>
</div><p>最後に、これらを統合してApollo Serverを起動します。Next.jsのAPI Routesを使うと以下のようになります。</p>
<p><strong><code>pages/api/graphql.ts</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">ApolloServer</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@apollo/server&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">startServerAndCreateNextHandler</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@as-integrations/next&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">gql</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;graphql-tag&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">resolvers</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../../server/resolvers&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">readFileSync</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;fs&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">path</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;path&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// スキーマファイルを読み込む
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">typeDefs</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gql</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">readFileSync</span>(<span style="color:#a6e22e">path</span>.<span style="color:#a6e22e">join</span>(<span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">cwd</span>(), <span style="color:#e6db74">&#39;schema.graphql&#39;</span>), <span style="color:#e6db74">&#39;utf8&#39;</span>)
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">server</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ApolloServer</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">typeDefs</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">resolvers</span>,
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">startServerAndCreateNextHandler</span>(<span style="color:#a6e22e">server</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで <code>/api/graphql</code> エンドポイントが完成しました。</p>
<h4 id="3-クライアントサイド実装-with-apollo-client">3. クライアントサイド実装 (with Apollo Client)</h4>
<p>クライアント側では、Apollo Clientを使ってサーバーと通信します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install @apollo/client graphql
</span></span></code></pre></td></tr></table>
</div>
</div><p>まず、Apollo Clientのインスタンスを作成し、Reactアプリケーション全体を<code>&lt;ApolloProvider&gt;</code>でラップします。</p>
<p><strong><code>pages/_app.tsx</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">ApolloClient</span>, <span style="color:#a6e22e">InMemoryCache</span>, <span style="color:#a6e22e">ApolloProvider</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@apollo/client&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ApolloClient</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">uri</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/api/graphql&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cache</span>: <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InMemoryCache</span>(),
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">MyApp</span>({ <span style="color:#a6e22e">Component</span>, <span style="color:#a6e22e">pageProps</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">ApolloProvider</span> <span style="color:#a6e22e">client</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">client</span>}&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Component</span> {<span style="color:#a6e22e">...pageProps</span>} /&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">ApolloProvider</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">MyApp</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>コンポーネント内では <code>useQuery</code> フックを使ってデータを取得します。</p>
<p><strong><code>components/UserList.tsx</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useQuery</span>, <span style="color:#a6e22e">gql</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@apollo/client&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">LIST_USERS_QUERY</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gql</span><span style="color:#e6db74">`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  query ListUsers {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    listUsers {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      id
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      name
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserList() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">loading</span>, <span style="color:#a6e22e">error</span>, <span style="color:#a6e22e">data</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">useQuery</span>(<span style="color:#a6e22e">LIST_USERS_QUERY</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">loading</span>) <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">p</span>&gt;<span style="color:#a6e22e">Loading</span>...&lt;/<span style="color:#f92672">p</span>&gt;;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">p</span>&gt;Error<span style="color:#f92672">:</span> {<span style="color:#a6e22e">error</span>.<span style="color:#a6e22e">message</span>}&lt;/<span style="color:#f92672">p</span>&gt;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">listUsers</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">user</span>: <span style="color:#66d9ef">any</span>) <span style="color:#f92672">=&gt;</span> ( <span style="color:#75715e">// ここ！ dataやuserの型がanyになっている
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        &lt;<span style="color:#f92672">li</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">id</span>}&gt;{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">name</span>}&lt;/<span style="color:#f92672">li</span>&gt;
</span></span><span style="display:flex;"><span>      ))}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="4-型安全性の実現-with-graphql-code-generator">4. 型安全性の実現 (with GraphQL Code Generator)</h4>
<p>上記のクライアントコードでは、<code>data</code> や <code>user</code> の型が <code>any</code> になっており、型安全ではありません。これを解決するのが <strong>GraphQL Code Generator</strong> です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
</span></span></code></pre></td></tr></table>
</div>
</div><p>設定ファイルを作成します。</p>
<p><strong><code>codegen.yml</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">schema</span>: <span style="color:#ae81ff">http://localhost:3000/api/graphql</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">documents</span>: <span style="color:#e6db74">&#34;./**/*.tsx&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">generates</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">./generated/graphql.ts</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">plugins</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">typescript</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">typescript-operations</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">typescript-react-apollo</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>package.json</code> にスクリプトを追加し、実行します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span><span style="color:#e6db74">&#34;scripts&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;codegen&#34;</span>: <span style="color:#e6db74">&#34;graphql-codegen&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm run codegen
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、クエリに対応した型定義とカスタムフックが <code>generated/graphql.ts</code> に自動生成されます。これを使うと、クライアントコードは以下のようになります。</p>
<p><strong><code>components/UserList.tsx</code> (after codegen)</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useListUsersQuery</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../generated/graphql&#39;</span>; <span style="color:#75715e">// 自動生成されたフックを利用
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserList() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// useQueryの代わりに、型付けされたカスタムフックを使用
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">loading</span>, <span style="color:#a6e22e">error</span>, <span style="color:#a6e22e">data</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">useListUsersQuery</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">loading</span>) <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">p</span>&gt;<span style="color:#a6e22e">Loading</span>...&lt;/<span style="color:#f92672">p</span>&gt;;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">p</span>&gt;Error<span style="color:#f92672">:</span> {<span style="color:#a6e22e">error</span>.<span style="color:#a6e22e">message</span>}&lt;/<span style="color:#f92672">p</span>&gt;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* data.listUsers や user.name にアクセスすると、型補完が効く！ */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#a6e22e">data</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">listUsers</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">user</span>) <span style="color:#f92672">=&gt;</span> (
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">li</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">id</span>}&gt;{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">name</span>}&lt;/<span style="color:#f92672">li</span>&gt;
</span></span><span style="display:flex;"><span>      ))}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで、フロントエンドでも完全に型安全な開発が可能になりました。しかし、この設定に至るまでには複数のライブラリのインストールと設定ファイルの記述、そして <code>codegen</code> コマンドの実行というステップが必要でした。</p>
<hr>
<h3 id="trpc型推論による究極のdx">tRPC：型推論による究極のDX</h3>
<p>tRPCは、TypeScriptの型推論を最大限に活用し、上記のようなスキーマ定義やコード生成を一切不要にします。</p>
<h4 id="1-図解trpcの処理フロー">1. 図解：tRPCの処理フロー</h4>
<p><em>tRPCの核心は、サーバーのRouter定義からエクスポートされた <code>AppRouter</code> 型を、クライアントが直接インポートして利用する点にあります。これにより、型情報がシームレスに共有されます。</em></p>
<h4 id="2-サーバーサイド実装-routerの定義">2. サーバーサイド実装 (Routerの定義)</h4>
<p>まずは必要なライブラリをインストールします。入力バリデーションのために <code>zod</code> を使うのが一般的です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に、APIのロジックを「ルーター」として定義します。これはGraphQLのリゾルバとスキーマを一緒にしたようなものです。</p>
<p><strong><code>server/trpc.ts</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">initTRPC</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@trpc/server&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">t</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">initTRPC</span>.<span style="color:#a6e22e">create</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">router</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">router</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">publicProcedure</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">procedure</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong><code>server/routers/_app.ts</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">z</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;zod&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">router</span>, <span style="color:#a6e22e">publicProcedure</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../trpc&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// サンプルのため、DBの代わりにインメモリ配列を使用
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">users</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1&#39;</span>, <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Alice&#39;</span>, <span style="color:#a6e22e">email</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;alice@example.com&#39;</span> },
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2&#39;</span>, <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Bob&#39;</span>, <span style="color:#a6e22e">email</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;bob@example.com&#39;</span> },
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">appRouter</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">router</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// クエリ (データ取得)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">getUser</span>: <span style="color:#66d9ef">publicProcedure</span>
</span></span><span style="display:flex;"><span>    .<span style="color:#a6e22e">input</span>(<span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({ <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">z.string</span>() })) <span style="color:#75715e">// 入力の型をzodで定義
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .<span style="color:#a6e22e">query</span>(({ <span style="color:#a6e22e">input</span> }) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// input は { id: string } 型と推論される
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">users</span>.<span style="color:#a6e22e">find</span>(<span style="color:#a6e22e">user</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">id</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">id</span>) <span style="color:#f92672">||</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }),
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">listUsers</span>: <span style="color:#66d9ef">publicProcedure</span>
</span></span><span style="display:flex;"><span>    .<span style="color:#a6e22e">query</span>(() <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">users</span>;
</span></span><span style="display:flex;"><span>    }),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ミューテーション (データ変更)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">createUser</span>: <span style="color:#66d9ef">publicProcedure</span>
</span></span><span style="display:flex;"><span>    .<span style="color:#a6e22e">input</span>(<span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">z.string</span>(), <span style="color:#a6e22e">email</span>: <span style="color:#66d9ef">z.string</span>().<span style="color:#a6e22e">optional</span>() }))
</span></span><span style="display:flex;"><span>    .<span style="color:#a6e22e">mutation</span>(({ <span style="color:#a6e22e">input</span> }) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// input は { name: string, email?: string } 型と推論される
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">newUser</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">String</span>(<span style="color:#a6e22e">users</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>), ...<span style="color:#a6e22e">input</span> };
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">users</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">newUser</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">newUser</span>;
</span></span><span style="display:flex;"><span>    }),
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// この型をエクスポートするのがtRPCの魔法の鍵
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">AppRouter</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">typeof</span> <span style="color:#a6e22e">appRouter</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>最後に、Next.jsのAPI RoutesでtRPCサーバーを公開します。</p>
<p><strong><code>pages/api/trpc/[trpc].ts</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createNextApiHandler</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@trpc/server/adapters/next&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">appRouter</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../../../server/routers/_app&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">createNextApiHandler</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">router</span>: <span style="color:#66d9ef">appRouter</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">createContext</span><span style="color:#f92672">:</span> () <span style="color:#f92672">=&gt;</span> ({}), <span style="color:#75715e">// 必要に応じてDB接続などを渡す
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>});
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="3-クライアントサイド実装-with-react-query">3. クライアントサイド実装 (with React Query)</h4>
<p>クライアント側の設定は非常にシンプルです。まず、tRPCクライアントを作成します。</p>
<p><strong><code>utils/trpc.ts</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">httpBatchLink</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@trpc/client&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createTRPCNext</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@trpc/next&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#66d9ef">type</span> { <span style="color:#a6e22e">AppRouter</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../server/routers/_app&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getBaseUrl() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">typeof</span> window <span style="color:#f92672">!==</span> <span style="color:#e6db74">&#39;undefined&#39;</span>) <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ... (サーバーサイドでのURL解決)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">return</span> <span style="color:#e6db74">`http://localhost:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">process</span>.<span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">PORT</span> <span style="color:#f92672">??</span> <span style="color:#ae81ff">3000</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">trpc</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">createTRPCNext</span>&lt;<span style="color:#f92672">AppRouter</span>&gt;({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">config() {</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">links</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">httpBatchLink</span>({
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">getBaseUrl</span>()<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/trpc`</span>,
</span></span><span style="display:flex;"><span>        }),
</span></span><span style="display:flex;"><span>      ],
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ssr</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>AppRouter</code> 型をサーバーから直接インポートしている点に注目してください。</p>
<p><code>_app.tsx</code> をtRPCでラップします。</p>
<p><strong><code>pages/_app.tsx</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">trpc</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../utils/trpc&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">MyApp</span>({ <span style="color:#a6e22e">Component</span>, <span style="color:#a6e22e">pageProps</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">Component</span> {<span style="color:#a6e22e">...pageProps</span>} /&gt;;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">trpc</span>.<span style="color:#a6e22e">withTRPC</span>(<span style="color:#a6e22e">MyApp</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで準備完了です。コンポーネントからAPIを呼び出してみましょう。</p>
<p><strong><code>components/UserList.tsx</code></strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">trpc</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;../utils/trpc&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserList() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// `trpc`オブジェクトから直接プロシージャを呼び出す！
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">data</span>: <span style="color:#66d9ef">users</span>, <span style="color:#a6e22e">isLoading</span>, <span style="color:#a6e22e">error</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">trpc</span>.<span style="color:#a6e22e">listUsers</span>.<span style="color:#a6e22e">useQuery</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">isLoading</span>) <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">p</span>&gt;<span style="color:#a6e22e">Loading</span>...&lt;/<span style="color:#f92672">p</span>&gt;;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">p</span>&gt;Error<span style="color:#f92672">:</span> {<span style="color:#a6e22e">error</span>.<span style="color:#a6e22e">message</span>}&lt;/<span style="color:#f92672">p</span>&gt;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        users の型は (typeof users)[] と完全に推論される！
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        user.id や user.name へのアクセスも型安全で、VSCodeの補完が完璧に機能する。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#a6e22e">users</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">user</span>) <span style="color:#f92672">=&gt;</span> (
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">li</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">id</span>}&gt;{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">name</span>}&lt;/<span style="color:#f92672">li</span>&gt;
</span></span><span style="display:flex;"><span>      ))}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>どうでしょうか。GraphQLで必要だったスキーマ定義、<code>gql</code>タグ、コード生成といったステップが一切ありません。サーバーの <code>appRouter</code> にプロシージャを追加・変更すれば、即座にクライアントの <code>trpc</code> オブジェクトに反映され、TypeScriptコンパイラが型チェックを行います。まるでバックエンドの関数を直接呼び出しているかのような、驚異的な開発体験です。</p>
<h2 id="メリットデメリット徹底比較">メリット・デメリット徹底比較</h2>
<p>実装例から見えてきた両者の特性を、メリット・デメリットとして整理し、比較表にまとめます。</p>
<h3 id="graphql">GraphQL</h3>
<h4 id="メリット">メリット</h4>
<ul>
<li><strong>言語・環境非依存</strong>: サーバーがGo、Python、Javaで、クライアントがTypeScript、Swift、Kotlinであっても連携可能です。これはGraphQLの最大の強みであり、多様な技術スタックが混在する環境では他に代えがたい利点です。</li>
<li><strong>強力なエコシステム</strong>: ApolloやRelayといった高機能なクライアントライブラリ、スキーマ管理ツール、GraphiQLのようなインタラクティブな開発コンソールなど、成熟したエコシステムが存在します。</li>
<li><strong>公開APIとしての実績</strong>: GitHub APIやShopify APIなど、多くの大規模な公開APIで採用されています。不特定多数のサードパーティ開発者にAPIを提供する際に、スキーマが厳密な契約書として機能します。</li>
<li><strong>スキーマフェデレーション</strong>: 複数のマイクロサービスを一つのGraphQL APIゲートウェイに統合する「Apollo Federation」のような仕組みがあり、大規模な分散システムと非常に相性が良いです。</li>
</ul>
<h4 id="デメリット">デメリット</h4>
<ul>
<li><strong>学習コスト</strong>: クエリ言語、スキーマ、リゾルバ、型システム、キャッシュ戦略など、学ぶべき独自の概念が多く、初学者にとってはハードルが高いです。</li>
<li><strong>複雑なセットアップ</strong>: サーバー、クライアント、コードジェネレーターなど、開発を始めるまでに多くの設定が必要です。</li>
<li><strong>パフォーマンス問題の潜在リスク</strong>: クライアントが自由にクエリを組み立てられるため、意図せず重いクエリ（N+1問題など）が実行される可能性があります。サーバーサイドでDataLoaderのような対策を講じる必要があります。</li>
<li><strong>キャッシュの複雑さ</strong>: 単一エンドポイントへのPOSTリクエストが基本となるため、RESTのような単純なHTTPレイヤーでのキャッシュが効きにくく、クライアントサイドでの正規化キャッシュなど高度な戦略が求められます。</li>
</ul>
<h3 id="trpc">tRPC</h3>
<h4 id="メリット-1">メリット</h4>
<ul>
<li><strong>究極の型安全性と開発体験 (DX)</strong>: コード生成が不要で、リアルタイムに型情報がフロントエンドとバックエンドで共有されます。サーバーのコードを変更した瞬間に、クライアントのコードで型エラーが検知されるため、バグを未然に防ぎ、リファクタリングも安全に行えます。オートコンプリートも完璧です。</li>
<li><strong>圧倒的なシンプルさと低学習コスト</strong>: APIの定義は、実質的にTypeScriptの関数をエクスポートする感覚に近いです。RESTやGraphQLのような独自の規約を学ぶ必要がほとんどなく、すぐに開発を始められます。</li>
<li><strong>軽量で高速</strong>: APIスキーマをパースしたり、複雑なクエリを解釈したりする必要がないため、ランタイムのオーバーヘッドが非常に小さいです。</li>
<li><strong>フルスタックTypeScriptとの最高の相性</strong>: Next.js, Remix, SvelteKitなどのフレームワークや、Turborepo, Nxのようなモノレポツールと組み合わせることで、その真価を最大限に発揮します。</li>
</ul>
<h4 id="デメリット-1">デメリット</h4>
<ul>
<li><strong>TypeScriptへの強い依存</strong>: サーバーとクライアントがTypeScript（またはJavaScript）であることが大前提です。ネイティブモバイルアプリや、異なる言語で書かれたバックエンドサービスと直接連携することはできません。これがtRPCの最大の制約です。</li>
<li><strong>公開APIには不向き</strong>: tRPCのプロトコルは内部利用を前提としており、不特定多数の外部開発者にAPIを提供するには適していません。（HTTP経由で呼び出すことは可能ですが、型安全性のメリットは失われます）。</li>
<li><strong>発展途上のエコシステム</strong>: GraphQLに比べると、歴史が浅く、サードパーティ製のツールやライブラリはまだ少ないです。ただし、コミュニティは非常に活発で、急速に成長しています。</li>
</ul>
<h3 id="比較まとめ表">比較まとめ表</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">項目</th>
          <th style="text-align: left">GraphQL</th>
          <th style="text-align: left">tRPC</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>主な用途</strong></td>
          <td style="text-align: left">公開API, モバイルアプリ, マイクロサービス連携</td>
          <td style="text-align: left">フルスタックTypeScriptアプリ, 社内ツール</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>型安全性</strong></td>
          <td style="text-align: left">スキーマとコード生成で実現</td>
          <td style="text-align: left">TypeScriptの型推論でネイティブに実現</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>開発体験 (DX)</strong></td>
          <td style="text-align: left">良好（ツールが多い）</td>
          <td style="text-align: left"><strong>最高</strong>（シンプル、即時性、補完）</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>学習コスト</strong></td>
          <td style="text-align: left">高い</td>
          <td style="text-align: left"><strong>低い</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>セットアップ</strong></td>
          <td style="text-align: left">複雑</td>
          <td style="text-align: left"><strong>シンプル</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>言語依存性</strong></td>
          <td style="text-align: left"><strong>なし</strong></td>
          <td style="text-align: left">TypeScriptに強く依存</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>エコシステム</strong></td>
          <td style="text-align: left"><strong>非常に成熟</strong></td>
          <td style="text-align: left">急速に成長中</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>コード生成</strong></td>
          <td style="text-align: left">必須</td>
          <td style="text-align: left"><strong>不要</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="現場で使える実践的なtips2026年のapi選定基準">現場で使える実践的なTips：2026年のAPI選定基準</h2>
<p>さて、これまでの比較を踏まえ、この記事の核心である「2026年を見据えたAPI選定基準」を、具体的なシナリオベースで提言します。</p>
<h3 id="あなたのプロジェクトに最適なのはどっち">あなたのプロジェクトに最適なのはどっち？</h3>
<h4 id="ケース1-nextjsでwebアプリケーションを新規開発する">ケース1: Next.jsでWebアプリケーションを新規開発する</h4>
<ul>
<li><strong>チーム</strong>: フロントエンドもバックエンドもTypeScriptで開発する。</li>
<li><strong>ユーザー</strong>: 主に自社のWebフロントエンド。</li>
<li><strong>結論</strong>: <strong>tRPCを第一候補として強く推奨します。</strong></li>
<li><strong>理由</strong>: 開発速度、保守性、そして何より開発体験が段違いです。APIの仕様変更に起因するバグを撲滅でき、チームの生産性を最大化できます。モノレポ構成にすれば、型定義の共有はさらに容易になります。2026年、この領域ではtRPCがデファクトスタンダードになっている可能性が非常に高いでしょう。</li>
</ul>
<h4 id="ケース2-不特定多数の開発者にapiを公開するpublic-api">ケース2: 不特定多数の開発者にAPIを公開する（Public API）</h4>
<ul>
<li><strong>チーム</strong>: バックエンドはGo、クライアントはWeb, Pythonスクリプト, etc&hellip;</li>
<li><strong>ユーザー</strong>: 社外のパートナー企業や一般の開発者。</li>
<li><strong>結論</strong>: <strong>GraphQL一択です。</strong></li>
<li><strong>理由</strong>: 言語非依存という最大の強みが活きます。厳密なスキーマは、それ自体が仕様書（ドキュメント）となり、APIの利用者に安定したインターフェースを提供します。エコシステムが成熟しているため、様々な言語でクライアントライブラリが利用できるのも大きな利点です。</li>
</ul>
<h4 id="ケース3-webiosandroidの3つのクライアントを持つサービスを開発する">ケース3: Web、iOS、Androidの3つのクライアントを持つサービスを開発する</h4>
<ul>
<li><strong>チーム</strong>: バックエンドはNode.js(TS)、フロントはNext.js(TS)、iOSはSwift、AndroidはKotlin。</li>
<li><strong>結論</strong>: <strong>GraphQLが最有力候補です。</strong></li>
<li><strong>理由</strong>: このシナリオも、言語の壁を越える必要があるためGraphQLの独壇場です。単一のGraphQLエンドポイントを用意すれば、各クライアントチームはそれぞれの言語用のコード生成ツールを使い、型安全な開発を進めることができます。</li>
</ul>
<h4 id="ケース4-複数のマイクロサービスを集約するbff-backend-for-frontend-を構築する">ケース4: 複数のマイクロサービスを集約するBFF (Backend for Frontend) を構築する</h4>
<ul>
<li><strong>チーム</strong>: 各マイクロサービスは独立して開発されている（言語もバラバラ）。</li>
<li><strong>ユーザー</strong>: フロントエンドチーム。</li>
<li><strong>結論</strong>: <strong>GraphQL (Apollo Federation) が非常に強力な選択肢です。</strong></li>
<li><strong>理由</strong>: 各マイクロサービスのAPI（RESTやgRPCなど）を、BFF層のGraphQLリゾルバでラッピングし、フロントエンドからは単一のデータグラフとして扱えるようにします。これにより、フロントエンドは複雑なマイクロサービスの構成を意識することなく、効率的にデータを取得できます。</li>
</ul>
<h3 id="ハイブリッドアプローチという選択肢">ハイブリッドアプローチという選択肢</h3>
<p>「うちのサービスは、社内Webアプリがメインだけど、一部の機能だけパートナー企業にAPIとして公開したい…」</p>
<p>このような場合、どちらか一方を選ぶ必要はありません。<strong>tRPCとGraphQLを共存させる</strong>ハイブリッドアプローチが有効です。</p>
<p>例えばNext.jsでは、以下のようにAPI Routesを分けることで簡単に実現できます。</p>
<ul>
<li><code>/api/trpc/[trpc]</code>: 社内Webアプリ用のメインAPI。tRPCで高速開発。</li>
<li><code>/api/graphql</code>: パートナー企業向けの公開API。GraphQLで安定したインターフェースを提供。</li>
</ul>
<p>このアプローチにより、それぞれの技術の「おいしいところ」だけを享受することができます。日々の開発はtRPCの圧倒的なDXの恩恵を受けつつ、疎結合性が求められる部分にはGraphQLの堅牢さを適用する。これは非常に現実的で強力な戦略です。</p>
<h2 id="まとめ">まとめ</h2>
<p>GraphQLとtRPCは、API開発における異なる課題を解決するために生まれた技術です。両者は単なる優劣で語れるものではなく、<strong>適材適所</strong>で使い分けるべき関係にあります。</p>
<ul>
<li><strong>tRPC</strong>は、<strong>フルスタックTypeScript</strong>という特定の、しかし非常に強力なエコシステムの中で、開発体験と型安全性を極限まで高めるための**「特化型」**ソリューションです。</li>
<li><strong>GraphQL</strong>は、言語や環境の壁を越え、多様なクライアントとサーバーを繋ぐための**「汎用型」**ソリューションです。</li>
</ul>
<p>2026年のAPI開発のトレンドを予測するならば、以下のようになるでしょう。</p>
<blockquote>
<p><strong>フルスタックTypeScriptで完結するプロジェクトではtRPCが主流となり、開発の高速化と品質向上を牽引する。一方で、マイクロサービス連携やPublic APIなど、システムの境界を越えるインターフェースとしては、引き続きGraphQLがその堅牢性と汎用性で重要な役割を担い続ける。</strong></p></blockquote>
<p>最終的にあなたが行うべき技術選定は、流行りの技術に飛びつくことではありません。あなたのプロジェクトが直面している課題は何か、APIの利用者は誰か、開発チームの構成はどうなっているか——これらの問いに真摯に向き合い、それぞれの技術が持つ本質的な価値を理解した上で、最適なツールを選択することです。</p>
<p>この記事が、あなたの次なる一歩を力強く後押しできれば幸いです。まずは小さなサイドプロジェクトでtRPCを試してみてください。きっと、そのあまりの快適さに驚かされるはずです。</p>
]]></content:encoded>
      <category>Backend</category>
      <category>GraphQL</category>
      <category>tRPC</category>
      <category>API</category>
    </item>
  </channel>
</rss>
