tRPC は REST API や GraphQL の代わりに利用することができる技術です。”t”は TypeScript の頭文字の T を表しており、RPC は Remote Procedure Call(リモートプロシージャーコール)を表しています。tRPC を利用することでバックエンドで定義した Procedure(関数)をフロントエンドで実行することができ、フロントエンドとバックエンドで TypeScript の型情報を共有することで型安全(Type Safe)な WEB アプリケーションを構築することができます。型定義の共有はバックエンド側の API のエンドポイントの Procedure の設定の中で行いサーバ側で export した情報をクライアントで import することで実現します。

tRPC を理解するためには動作確認を行うことが一番の近道だと思うので本文書ではバックエンド側に Express を利用して tRPC サーバ、クライアント側では React を利用して tRPC クライアントの設定を行います。tRPC サーバ側で設定した API エンドポイントを通してクライアントからデータ処理が行えること、エディタに VS Code を利用して型情報の共有とはどのようなものなのかを確認しています。さらに Context(React Context API ではありません)を利用したデータベースの接続の方法、データベースへのデータの追加方法についても説明を行っています。難しいコードは一切利用していないので本文書を読み終えると tRPC はどのようなものか確実に理解することができます。

サーバの設定

tRPCではサーバとクライアントの設定を行うため任意の名前のフォルダを作成してそのフォルダの下にサーバ用のフォルダとクライアント用のフォルダを作成していきます。

ここではmy-trpcというフォルダ名をつけています。


 % mkdir my-trpc

my-trpcフォルダ作成後、my-trpcフォルダに移動してサーバのコードを保存するserverフォルダの作成を行います。後ほどmy-trpcフォルダにはclientフォルダを作成します。


 % cd my-trpc 
 % mkdir server

Expressサーバの環境構築

作成したserverフォルダに移動してnpm init -yコマンドを実行します。npm init -yコマンドを実行するとpackage.jsonファイルが作成されます。


 % cd server
 % npm init -y

Express サーバを構築するために必要なパッケージ以外にも tRPC サーバの構築を行うためには tRPC に関するいくつかのパッケージをインストールする必要があります。

express、cors と一緒に tRPC サーバの設定に必要な@trpc/server と zod をインストールします。cors は後ほど設定する React クライアントからのアクセスを許可する際に利用します。zod はバリデーションライブラリです。クライアント側から送信されてきたデータがサーバ側で要求された条件や形式を満たしているかを確認するバリデーションすで利用します。


 % npm install express cors @trpc/server zod

さらに TypeScript を利用するために必要なパッケージのインストールを行います。nodemon はファイルの更新を監視するために利用するため必須ではありませんがインストールを行っています。


 % npm install --save-dev typescript nodemon ts-node @types/express @types/node @types/cors

Express/tRPCサーバを設定するメインファイルとしてindex.tsファイルを作成します。index.tsファイルを作成後、package.jsonファイルの更新を行います。


 % touch index.ts

package.jsonファイルには今回利用した各種パッケージのバージョンも確認することができます。scriptsにdevを追加してnodemonでindex.tsファイルの更新の監視設定を行っています。


{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "dev": "nodemon index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@trpc/server": "^10.9.0",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "zod": "^3.20.2"
  },
  "devDependencies": {
    "@types/cors": "^2.8.13",
    "@types/express": "^4.17.15",
    "@types/node": "^18.11.18",
    "nodemon": "^2.0.20",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  }
}

package.json ファイルの更新が完了すると”npm run dev”コマンドを実行することができます。 実行時に表示されているメッセージを見ると nodemon を実行すると ts-node が実行されていることも確認できます。ts-node は TypeScript から JavaScript の変換を行い、TypeScript ファイルを直接実行することができます。ts-node を利用しな場合は一度 tsc コマンドで TypeScript ファイルを JavaScript に変換し、変換によって生成された JavaScript ファイルを node コマンドで実行します。ts-node コマンドを利用することで効率的に開発を行うことができます。


 % npm run dev

> server@1.0.0 dev
> nodemon index.ts

[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node index.ts`
[nodemon] clean exit - waiting for changes before restart

Expressサーバの動作確認

Expressサーバの動作確認を行うために以下のコードを記述します。


import express from 'express';

const app = express();
const PORT = 3000;

app.get('/', (_req, res) => res.send('hello'));

app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`));

ブラウザからlocalhost:3000にアクセスして画面上に”hello”が表示されればExpressサーバは正常に動作しています。

trpcサーバの設定

最もシンプルなコードでtprcサーバの設定を行います。コードの中身の説明は後ほど行っています。


import express from 'express';
import { initTRPC } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';

const app = express();
const PORT = 3000;

const t = initTRPC.create();

const appRouter = t.router({
  hello: t.procedure.query(() => {
    return 'Hello World';
  }),
});

app.get('/', (_req, res) => res.send('hello')); //削除可能

app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
  })
);

app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`));

export type AppRouter = typeof appRouter;

trpcサーバが正常に動作している場合、ブラウザから/trpc/helloにアクセスを行うとブラウザ上には以下のJSONオブジェクトが表示されます。

/trpc/helloへのアクセス
/trpc/helloへのアクセス

trpc サーバが正常に動作していることが確認できたのでメインファイルの index.ts のコードの中身の確認を行います。

@trpc/server から import した initTRPC の create メソッドで tRPC インスタンスの初期化を行っています。REST API でルーティングを設定するのと同様に tRPC でもルーティングの設定を行います。ルーティングの設定は t.router を使って、クライアントからアクセス可能な API エンドポイントの設定を行います。下記のコードでは hello という一つのルーティングしか設定していませんが複数の API エンドポイントを設定することができます。


import { initTRPC } from '@trpc/server';
//略

const t = initTRPC.create();

const appRouter = t.router({
  hello: t.procedure.query(() => {
    return 'Hello World';
  }),
});

API エンドポイントでは Procedure の設定を行います。Procedure は関数で、ここでは hello という名前で hello Prodecure の設定を行っています。hello Procedure の設定によりクライアントから/trpc/hello でアクセスすることが可能となります。また hello という名前はクライアント側の処理で利用します。hello の前についている/trpc については Express 用に提供されている Adapter を利用した middleware の設定を行うことで trpc で設定したルーティングを Express サーバで利用できるようになります。


app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
  })
);
tRPCではExpressだけではなくNext.js, FastifyなどのAdapterが提供されています。

hello Procedureのqueryでは引数にresolver関数を取ります。resolver関数の戻り値でクライアントに戻す”Hello World”を設定しています。


hello: t.procedure.query(() => {
  return 'Hello World';
}),

最終行で設定したRouterの型をクライアントで利用できるようにexportしています。


export type AppRouter = typeof appRouter;

これが最もシンプルなExpressにおけるtrpcサーバの設定です。

クライアントの設定

バックエンドのExpressサーバの設定が完了したのでフロントエンドのReactの設定を行なっていきます。

Reactプロジェクトの作成

Vite を利用して React プロジェクトの作成を行うので my-trpc フォルダの中で npm create vite@latest コマンドを実行します。プロジェクトはコマンドの最後に設定した client フォルダの中に保存されます。コマンドを実行すると framework, variant の選択を行います。本文書では framework では React, variant では TypeScript を選択します。


 % npm create vite@latest client
? Select a framework: › - Use arrow-keys. Return to submit.
    Vanilla
    Vue
❯   React
    Preact
    Lit
    Svelte
    Others
? Select a variant: › - Use arrow-keys. Return to submit.
    JavaScript
❯   TypeScript
    JavaScript + SWC
    TypeScript + SWC
Scaffolding project in /Users/mac/Desktop/my-trpc/client...

Done. Now run:

  cd client
  npm install
  npm run dev

npm create viteコマンド完了後、my-trpcフォルダにclientフォルダが作成されるのでclientフォルダに移動してnpm installコマンドを実行します。my-trpcフォルダにはサーバ用のserverとクライアント用のclientフォルダが存在することになります。


% cd client
% npm install

tRPCクライアント設定

React上でtRPCクライアントとして利用するためには@trpc/client, @trpc/serverに加えてtanstackのreact-queryをインストールといくつかの設定が必要となります。インストールと設定についてはドキュメントを参考に行っています。

まず必要なパッケージのインストールをclientフォルダで行います。


 % npm install @trpc/client @trpc/server @trpc/react-query @tanstack/react-query

パッケージのインストール後、client フォルダの src フォルダに utils フォルダを作成して trpc.ts ファイルで React Query Hook の設定を行います。server 側で設定した index.ts ファイルでは最終行で type AppRouter を export していました。その export した type を trpc.ts ファイルで import しています。


import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../../server/index';
export const trpc = createTRPCReact<AppRouter>();

componentsフォルダを作成して動作確認用のコンポーネントTest.tsxファイルを作成して以下のコードを記述します。


const Test = () => {
  return <div>Test</div>;
};

export default Test;

Test.tsxファイル作成後、App.tsxファイルの中でtRPCとReact Queryを利用してContext APIのProvidersの設定を行います。


import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from './utils/trpc';
import Test from './components/Test';

function App() {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/trpc',
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <Test />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

export default App;

srcフォルダのmain.tsxファイルでimportされているindex.cssの行を削除しておきます。tRPCクライアントを利用する上で削除は必須ではありませんがindex.cssに記述されているCSSが適用されるのを防ぐためです。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
// import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

clientフォルダでnpm run devコマンドを実行するとViteの開発サーバが起動します。起動メッセージにエラーが表示されていないことを確認します。


 % npm run dev
  VITE v4.0.4  ready in 592 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

ブラウザからhttp://127.0.0.1:5173/にアクセスを行います。Testコンポーネントに記述した”Test”の文字列が表示されることが確認できます。

Testコンポーネントの内容が表示
Testコンポーネントの内容が表示

初めてのtRPCクライアントからのアクセス

Test コンポーネント上で設定した tRPC クライアントを利用してサーバ上で設定した API エンドポイントの/trpc/hello へアクセスを行います。どのようなデータが戻されるのか確認するために取得したデータは hello に保存して console.log でブラウザのコンソールに表示するように設定しています。データを取得するために utils フォルダの trpc.ts に保存した trpc を import してサーバ側で設定した Procedure の hello を設定し、それにつづけて useQuery を設定します。


import { trpc } from '../utils/trpc';

const Test = () => {
  const hello = trpc.hello.useQuery();
  console.log(hello);
  return <div>Test</div>;
};

export default Test;

設定後、Expressサーバが起動していることを確認してhttp://127.0.0.1:5173/にアクセスするとCORSに関するメッセージが表示されます。サーバとクライアントで起動しているサーバのPORTが異なるためにCORSのエラーが発生しています。


Access to fetch at 'http://localhost:3000/trpc/hello?batch=1&input=%7B%7D' from origin 'http://127.0.0.1:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

server側のindex.tsファイルでCORSの設定を行います。CORSはtRPCサーバを設定する際にインストール済みです。


import express from 'express';
import { initTRPC } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';

const app = express();
const PORT = 3000;

app.use(cors());
//略

再度http://127.0.0.1:5173/にアクセスを行うとブラウザのコンソールに以下のオブジェクトが表示されます。useQueryを利用しているためオブジェクトの中にはdataだけではなくerrorやisLoadingやisSuccessなどの値を持っていることが確認できます。

helloエンドポイントから取得したデータの確認
helloエンドポイントから取得したデータの確認

デベロッパーツールのコンソールを確認すると/trpc/helloへネットワークリクエストが送信されていることが確認できます。

ネットワークタブの確認
ネットワークタブの確認

取得したオブジェクトの内容から取得したデータをブラウザ上に表示させたい場合には以下のように行うことができます。


import { trpc } from '../utils/trpc';

const Test = () => {
  const hello = trpc.hello.useQuery();
  return <div>{hello.data}</div>;
};

export default Test;

ブラウザ上には”Hello World”が表示されます。

取得したデータをブラウザ上に表示
取得したデータをブラウザ上に表示

tRPCクライアントを利用してtRPCサーバにアクセスを行い、データを取得、表示することができました。

React Query(TanStack Query)を利用しない場合

React Query を利用して設定を行いましたが React Query を利用しない場合の設定方法も確認しておきます。

App.tsx ファイルは React Query の設定がないので下記のようになります。


import Test from './components/Test';

function App() {
  return <Test />;
}

export default App;

Testコンポーネントの中でtrpcの設定を行っていますが下記のように記述することができます。


import { AppRouter } from '../../../server';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { useEffect } from 'react';

const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});
const Test = () => {
  useEffect(() => {
    const getHello = async () => {
      const hello = await client.hello.query();
      console.log(hello);
    };
    getHello();
  }, []);

  return <div>Test</div>;
};

export default Test;

ブラウザのコンソールを確認すると”Hello World”が表示されていることが確認できます。

httplink vs httpBatchLink

ここまでの設定の中で httpBatchLink を説明なしで利用していましたが名前に Batch があるように複数のリクエストを一つにまとめることができます。

React Query を利用しない場合のコードで下記のように複数の client.hello qeury を実行します


import { AppRouter } from '../../../server';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { useEffect } from 'react';

const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});
const Test = () => {
  useEffect(() => {
    const getHello = async () => {
      client.hello.query();
      client.hello.query();
      client.hello.query();
    };
    getHello();
  }, []);

  return <div>Test</div>;
};

export default Test;

ネットワークタブを確認します。リクエストが3つ送信されるのではなく一度で3つのリクエストの情報を含んだリクエストを送信していることが確認できます。Request URLが/hello, hello, hello?…になっていることが確認できます。

httpBatchLinkを利用した場合
httpBatchLinkを利用した場合

戻されるデータにも3つのリクエストの情報が含まれていることが確認できます。

戻されるデータの確認
戻されるデータの確認
client.hello.query()の前にawaitをつけるとhttpBatchLinkを利用しても別々のリクエストが送信されます。

Promise.allを利用することで戻されたデータを利用することができます。


useEffect(() => {
  const getHello = async () => {
    const hellos = await Promise.all([
      client.hello.query(),
      client.hello.query(),
      client.hello.query(),
    ]);
    console.log(hellos);
  };
  getHello();
}, []);

コンソールには下記のように表示されます。

Promise.allを利用して取得したデータ
Promise.allを利用して取得したデータ

httpBatchLinkではなくhttpLinkを利用した場合の動作確認を行います。


import { AppRouter } from '../../../server';
import { createTRPCProxyClient, httpLink } from '@trpc/client';
import { useEffect } from 'react';

const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});
const Test = () => {
  useEffect(() => {
    const getHello = async () => {
      client.hello.query();
      client.hello.query();
      client.hello.query();
    };
    getHello();
  }, []);

  return <div>Test</div>;
};

export default Test;

httpBatchLinkとは異なり3つのリクエストが送信されていることが確認できます。

httpLinkを利用した場合
httpLinkを利用した場合

httpBatchLinkを利用することで複数のリクエストを1つにまとめて送信することができることがわかりました。

クライアントからデータを送信

React Query を利用したコードを利用して動作確認を行なっていきます。

最も簡単な例を利用した動作確認では tRPC サーバでは tRPC クライアントからのデータを受け取っておらず, ただ文字列を返すだけのものでした。ここでは tRPC サーバ上でクライアントから受け取ることができるデータの定義を行うため、server フォルダの index.ts ファイルを更新します。

ルーティングに新たに helloName を追加します。hello と比較するとすぐにわかるように t.procedure に input が追加されています。input では Procedure が受け取るデータを定義することができます。バリデーションライブラリの Zod を利用してデータのチェックを行っています。定義した通りのデータを受け取った場合は query の resolver 関数の引数に input が渡されるので input の内容を利用して処理を行うことができます。ここはシンプルに受け取った name の値をクライアント側に戻しています。


const appRouter = t.router({
  hello: t.procedure.query(() => 'Hello World'),
  helloName: t.procedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return `Hello World ${input.name}`;
    }),
});

Zodのバリデーションライブラリとして利用方法については下記の記事で公開しているので参考にしてみてください。

最も簡単な例を利用した動作確認では tRPC サーバでは tRPC クライアントからのデータを受け取っておらず, ただ文字列を返すだけのものでした。ここでは tRPC サーバ上でクライアントから受け取ることができるデータの定義を行うため、server フォルダの index.ts ファイルを更新します。

ルーティングに新たに helloName を追加します。hello と比較するとすぐにわかるように t.procedure に input が追加されています。input では Procedure が受け取るデータを定義することができます。バリデーションライブラリの Zod を利用してデータのチェックを行っています。定義した通りのデータを受け取った場合は query の resolver 関数の引数に input が渡されるので input の内容を利用して処理を行うことができます。ここはシンプルに受け取った name の値をクライアント側に戻しています。

helloNameの選択
helloNameの選択

helloNameを選択後、useQueryを選択します。

useQueryを選択
useQueryを選択

useQueryを選択するとuseQueryの引数に設定するオブジェクトの情報が表示されます。

useQueryの引数に入れるオブジェクトの情報
useQueryの引数に入れるオブジェクトの情報

表示されている内容通り、nameプロパティと値を設定してhttp://127.0.0.1:5173/にアクセスを行います。


import { trpc } from '../utils/trpc';

const Test = () => {
  const test = trpc.helloName.useQuery({ name: 'John' });

  return <div>{test.data}</div>;
};

export default Test;

ブラウザで確認するとuseQueryの引数で設定した値が表示されていることが確認できます。

クライアントから送信したデータの表示
クライアントから送信したデータの表示

nameプロパティとは異なる名前を設定した場合には以下のメッセージが表示されるのでnameプロパティを設定する必要があることがわかります。

useQueryのプロパティ名を変える
useQueryのプロパティ名を変える

nameプロパティの値に異なる型を設定した場合にもメッセージが表示されます。

異なる型を設定した場合
異なる型を設定した場合

クライアント側ではなくサーバ側で input での定義を変更します。name は文字列の String, age は数値の number を設定しています。


const appRouter = t.router({
  hello: t.procedure.query(() => 'Hello World'),
  helloName: t.procedure
    .input(z.object({ name: z.string(), age: z.number() }))
    .query(({ input }) => {
      return `Hello World ${input.name}`;
    }),
});

更新した情報はすぐにクライアント側に反映されます。nameプロパティだけではメッセージが表示され、number型のageが必要であることがわかります。

サーバ側での更新後のクライアント側でのメッセージ
サーバ側での更新後のクライアント側でのメッセージ

nameとageプロパティの値を設定した後にtest.dataにカーソルを当てるとdataの型も確認することができます。

dataに含まれる型の確認
dataに含まれる型の確認

サーバ側でクライアントに戻すデータの内容を文字列からオブジェクトに変更します。


const appRouter = t.router({
  hello: t.procedure.query(() => 'Hello World'),
  helloName: t.procedure
    .input(z.object({ name: z.string(), age: z.number() }))
    .query(({ input }) => {
      return {
        greeting: `Hello World ${input.name}`,
        age: input.age,
      };
    }),
});

変更後にtest.dataにカーソルを当てるとサーバから戻されるオブジェクトの内容と型を確認することができます。

戻されるオブジェクトの内容の確認
戻されるオブジェクトの内容の確認

ここまでの動作確認を通して”tRPCを利用することでフロントエンドとバックエンドでTypeScriptの型情報を共有する”という理解が進んだのではないでしょうか。

Contextを利用したデータベースアクセス

Context を設定することですべての Procedure からアクセスすることができるデータを保持することができるためデータベースの接続情報や認証情報に利用することができます。

実際に Context にデータベースの接続情報を保持することで Context の利用方法を確認します。データベースには SQLite を利用しますが Express から SQLite にアクセスは Prisma を通して行います。

Prismaの設定

Prismaのインストールはserverフォルダで行います。


 % npm install prisma --save-dev

インストール後にPrismaの設定ファイルを作成するために初期化コマンド(npx prisma init)を実行します。接続するデータベースがSQLiteなのでオプションの–datasource-provider sqliteをつけます。


 % npx prisma init --datasource-provider sqlite

コマンドを実行するとprismaフォルダと.envファイルが作成されます。prismaフォルダにはPrismaの設定ファイルschema.prismaが作成されます。schema.prismaファイルにデータベースへの接続情報やデータモデルを記述します。データモデルはテーブルの構成情報です。


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

SQLite の設定になっているので上記の設定はそのままでデータモデルを記述します。データモデルの記述はデータベースによって異なるのでテーブルに追加した列の型の設定方法やリレーションシップなど不明な場合は Prisma のドキュメントを参考に設定を行ってください。

ここでは Todo テーブルを作成するので以下を記述します。Todo テーブルは id, name, isCompleted の 3 つの列を持ち、id は自動で順番に番号が付与されます。name は Todo の名前で文字列、isCompleted は Todo が完了したかどうかを表し、true や false の boolean 値のみ設定できます。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id      Int      @id @default(autoincrement())
  name   String
  isComplete Boolean @default(value: false)
}

schema.prismaの設定が完了したらデータベースファイル(SQLiteはファイルベースのデータベース)とテーブルを作成するためnpx prisma db pushコマンドを実行します。コマンドを実行するとprismaフォルダにdev.dbファイルが作成されます。


 % npx prisma db push

テーブルが作成できているかどうかはPrisma Studioを利用することができます。SQLiteのデータベースへの接続はコマンドによる接続やデータベースの管理ソフト(例:TablePlus)によっても行うことができます。


 % npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

Prisma Studioからはデータの閲覧だけではなくデータの追加もできるので2件のデータ追加を行います。Prisma Studioからはデータの追加だけではなく削除や更新も可能です。

Prisma Studioによるデータ挿入
Prisma Studioによるデータ挿入

Prismaの設定は完了です。

Contextの設定

serverフォルダのindex.tsファイルでPrismaClientのimportを行いprismインスタンスを作成します。


import { PrismaClient } from '@prisma/client';
//略
const prisma = new PrismaClient();

Contextの設定を行うためにcreateContext関数の作成を行います。関数の引数にはoptsが渡されます。optsオブジェクトにはreq, resオブジェクトを持っています。そのため認証情報などをヘッダーに含まれている場合にはopts.req.headersから取得することができます。


import * as trpcExpress from '@trpc/server/adapters/express';
//略
const createContext = (opts: trpcExpress.CreateExpressContextOptions) => {
//略
};

createContext関数の戻り値をProcedureからアクセスすることができるのでprismaインスタンスの情報を戻り値に設定します。


import express from 'express';
import { inferAsyncReturnType, initTRPC } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';
import { z } from 'zod';
import { PrismaClient } from '@prisma/client';

const app = express();
const PORT = 3000;

app.use(cors());

const prisma = new PrismaClient();

const createContext = (opts: trpcExpress.CreateExpressContextOptions) => {
  return { prisma };
};
//略

Contextを利用する場合はinitTPRCのcreateメソッドの前にcontextを設定する必要があります。


import { inferAsyncReturnType, initTRPC } from '@trpc/server';
//略
type Context = inferAsyncReturnType;
const t = initTRPC.context().create();

ここまでの設定でコードは以下のようになります。


import express from 'express';
import { inferAsyncReturnType, initTRPC } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';
import { z } from 'zod';
import { PrismaClient } from '@prisma/client';
const app = express();
const PORT = 3000;
app.use(cors());
const prisma = new PrismaClient();
const createContext = (opts: trpcExpress.CreateExpressContextOptions) => {
  return { prisma };
};
type Context = inferAsyncReturnType;
const t = initTRPC.context().create();
//略

ProcedureからのContextへのアクセス

Procedureのtodosを新たにルーティングに追加します。Contextはqueryのresolver関数の引数に含まれており、ctx.prismaでcreateContext関数の戻り値であるprismaにアクセスすることができます。


const appRouter = t.router({
  hello: t.procedure.query(() => {
    return 'Hello World';
  }),
  helloName: t.procedure
    //略
    }),
  todos: t.procedure.query(async ({ ctx }) => {
    const todos = await ctx.prisma.todo.findMany();
    return todos;
  }),
});

prisma.todo.findManyメソッドを利用することでSQLiteに保存されているTodoテーブルの全データを取得します。

サーバ側での設定が完了したら次はクライアント側で設定を行います。

クライアントからのデータ取得

Test.tsxファイルに以下のコードを記述します。


import { trpc } from '../utils/trpc';

const Test = () => {
  const { data: todos } = trpc.todos.useQuery();

  return (
    <ul>
      {todos?.map((todo) => (
        <li key={todo.id}>{todo.name}</li>
      ))}
    </ul>
  );
};

export default Test;

ブラウザからhttp://127.0.0.1:5173/にアクセスするとSQLiteデータベースに保存したTodoの情報が表示されます。

ブラウザ上に表示されたTodo一覧
ブラウザ上に表示されたTodo一覧

取得したデータのオブジェクトの内容がわからなくても型情報が共有されているので以下のように表示されるので正しいプロパティを設定することができます。

todoオブジェクトのプロパティの表示
todoオブジェクトのプロパティの表示

データベースへの接続情報を通してContextの設定方法を理解することができました。

headersに含まれる情報をContextで取得したい場合の方法も確認しておきます。クライアントではhttpBatchLinkの中でheaders関数の設定を行うことができます。


const [trpcClient] = useState(() =>
  trpc.createClient({
    links: [
      httpBatchLink({
        url: 'http://localhost:3000/trpc',
        headers() {
          return {
            Authorization: 'abcedf',
          };
        }
      }),
    ],
  })
);

先ほど説明した通りcreateContext関数のoptsオブジェクトの中のreqオブジェクトからheadersにアクセスすることができます。


const createContext = (opts: trpcExpress.CreateExpressContextOptions) => {
  console.log(opts.req.headers);
  return { prisma };
};

ブラウザからhttp://127.0.0.1:5173/するopts.req.headersオブジェクトの内容がサーバ側のターミナルに表示されます。headersオブジェクトの中にauthorizationプロパティが含まれ値が”abcedf”であることが確認できます。


{
  host: 'localhost:3000',
  connection: 'keep-alive',
  'sec-ch-ua': '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"',
  'content-type': 'application/json',
  'sec-ch-ua-mobile': '?0',
  authorization: 'abcedf',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
  'sec-ch-ua-platform': '"macOS"',
  accept: '*/*',
  origin: 'http://127.0.0.1:5173',
  'sec-fetch-site': 'cross-site',
  'sec-fetch-mode': 'cors',
  'sec-fetch-dest': 'empty',
  referer: 'http://127.0.0.1:5173/',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'ja,en-US;q=0.9,en;q=0.8'
}

データベースへのデータ追加

データベースからのデータを取得することができたので次はデータの追加方法について確認を行なっていきます。

サーバ側のルーティングに新たにaddTodo Procedureを追加します。addTodo Procedureで受け取るべき値はinputでチェックを行い、これまではqueryを利用していましたがデータの追加を行うのでmutationを利用します。mutationの引数のresolver関数でデータベースへの追加処理を行います。resolver関数の引数ではCotextとinputを利用します。


const appRouter = t.router({
  hello: t.procedure.query(() => {
    return 'Hello World';
  }),
  helloName: t.procedure
  //略
  todos: t.procedure.query(async ({ ctx }) => {
    const todos = await ctx.prisma.todo.findMany();
    return todos;
  }),
  addTodo: t.procedure
    .input(
      z.object({
        name: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const todo = await ctx.prisma.todo.create({
        data: input,
      });
      return todo;
    }),
});

サーバ側の設定が完了したらクライアント側での設定を行います。データベースに追加するデータをユーザに入力してもらうため input 要素を追加します。input 要素に onKeyDown イベントを設定して Enter ボタンを押し、input 要素に値が含まれている場合のみコンソールに入力した文字列を表示できるように設定しています。


import { trpc } from '../utils/trpc';

const Test = () => {
  const { data: todos } = trpc.todos.useQuery();

  const handleKeyDown = (e) => {
    const name = e.target.value;
    if (e.key === 'Enter' && name) {
      console.log('name', name);
    }
  };

  return (
    <>
      <h1>Todo</h1>
      <div>
        <label id="name">Add Todo:</label>
        <input name="name" onKeyDown={handleKeyDown} />
      </div>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>{todo.name}</li>
        ))}
      </ul>
    </>
  );
};

export default Test;

コードを更新後ブラウザで確認するとinput要素が表示されるので文字列を入力して”Enter”ボタンをクリックするとコンソールに入力した文字列が表示されることを確認してください。

入力フォームの追加
入力フォームの追加

入力した文字列が表示されることが確認できたら入力した文字列をtRPCサーバに送信する処理を追加します。

これまではuseQuery Hookを利用していましたがuseMutation Hookに更新して以下のように設定を行います。


const addTodo = trpc.addTodo.useMutation();

const handleKeyDown = (e) => {
  const name = e.target.value;
  if (e.key === 'Enter' && name) {
    addTodo.mutate({ name });
    e.target.value = '';
  }
};

tRPCでのReact Queryの利用方法についてはドキュメント(tRPC React Query documentation)を確認することをお勧めします。

tRPC React Query documentation
tRPC React Query documentation

useMutationを追加後、ブラウザ上に文字列を入力して”Enter”ボタンをクリックすると何も変化がありませんがページをリロードすると入力した文字列が追加されていることが確認できます。

追加したTodoの表示
追加したTodoの表示

入力後に追加した情報がブラウザ上に反映させるためuseContext HookとuseMutation Hookが持つオプションのonSuccess Callbackを利用します。useContext Hookはヘルパー関数でReact Queryで実行したクエリーのキャッシュデータにアクセスすることができます。onSuccessはuseMutationが成功した場合に実行される関数です。onSuccess以外にエラーが発生した場合に実行できるonErrorなどがあります。


const { data: todos } = trpc.todos.useQuery();

const utils = trpc.useContext();

const addTodo = trpc.addTodo.useMutation({
  onSuccess: () => {
    utils.todos.invalidate();
  },
});

const handleKeyDown = (e) => {
  const name = e.target.value;
  if (e.key === 'Enter' && name) {
    addTodo.mutate({ name });
    e.target.value = '';
  }
};

onSuccessではuseContextを利用してデータ取得のtodosのクエリーにアクセスを行い、invalidateメソッドでキャッシュにあるデータを無効にしてクエリーの再実行を行います。

input要素に文字列を入力して”Enter”ボタンを押すと入力した文字列がブラウザ上に反映されることを確認してください。

useContextを利用したリフェッチの確認
useContextを利用したリフェッチの確認

データ取得(useQuery)に続き、tRPCでのデータの追加方法(useMutation)の利用方法を確認することができました。