クイックスタート
tRPCはRESTとGraphQLの概念を組み合わせています。どちらにも馴染みがない場合は、キーとなる概念をご覧ください。
インストール
tRPCは複数のパッケージに分割されているため、必要なものだけをインストールできます。コードベースの適切なセクションに必要なパッケージをインストールするようにしてください。このクイックスタートガイドでは、シンプルさを保ち、バニラクライアントのみを使用します。フレームワークガイドについては、Reactでの使用とNext.jsでの使用を確認してください。
- tRPCにはTypeScript >= 4.7.0が必要です
- 公式にはstrictモード以外はサポートしていないため、
tsconfig.json
で"strict": true
を使用することを強く推奨します。
まず、@trpc/server
パッケージと@trpc/client
パッケージをインストールすることから始めましょう
- npm
- yarn
- pnpm
- bun
npm install @trpc/server@next @trpc/client@next
yarn add @trpc/server@next @trpc/client@next
pnpm add @trpc/server@next @trpc/client@next
bun add @trpc/server@next @trpc/client@next
バックエンドルーターの定義
tRPCを使用してタイプセーフなAPIを構築する手順を見ていきましょう。まず、このAPIには次のTypeScriptシグネチャを持つ3つのエンドポイントが含まれます
ts
type User = { id: string; name: string; };userList: () => User[];userById: (id: string) => User;userCreate: (data: { name: string }) => User;
ts
type User = { id: string; name: string; };userList: () => User[];userById: (id: string) => User;userCreate: (data: { name: string }) => User;
1. ルーターインスタンスの作成
まず、tRPCバックエンドを初期化しましょう。tRPCオブジェクト全体ではなく、別のファイルでこれを行い、再利用可能なヘルパー関数をエクスポートするのが良い慣習です。
server/trpc.tsts
import {initTRPC } from '@trpc/server';/*** Initialization of tRPC backend* Should be done only once per backend!*/constt =initTRPC .create ();/*** Export reusable router and procedure helpers* that can be used throughout the router*/export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/trpc.tsts
import {initTRPC } from '@trpc/server';/*** Initialization of tRPC backend* Should be done only once per backend!*/constt =initTRPC .create ();/*** Export reusable router and procedure helpers* that can be used throughout the router*/export constrouter =t .router ;export constpublicProcedure =t .procedure ;
次に、後でプロシージャを追加する、一般的にappRouter
と呼ばれるメインルーターインスタンスを初期化します。最後に、クライアント側で後で使用するルーターのタイプをエクスポートする必要があります。
server/index.tsts
import {router } from './trpc';constappRouter =router ({// ...});// Export type router type signature,// NOT the router itself.export typeAppRouter = typeofappRouter ;
server/index.tsts
import {router } from './trpc';constappRouter =router ({// ...});// Export type router type signature,// NOT the router itself.export typeAppRouter = typeofappRouter ;
2. クエリプロシージャの追加
publicProcedure.query()
を使用して、クエリプロシージャをルーターに追加します。
以下は、データベースからユーザーのリストを返すuserList
という名前のクエリプロシージャを作成します
server/index.tsts
import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({userList :publicProcedure .query (async () => {// Retrieve users from a datasource, this is an imaginary databaseconstusers = awaitdb .user .findMany ();returnusers ;}),});
server/index.tsts
import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({userList :publicProcedure .query (async () => {// Retrieve users from a datasource, this is an imaginary databaseconstusers = awaitdb .user .findMany ();returnusers ;}),});
3. 入力パーサーを使用したプロシージャ入力の検証
userById
プロシージャを実装するには、クライアントからの入力を受け入れる必要があります。tRPCでは、入力を検証および解析するための入力パーサーを定義できます。独自の入力パーサーを定義することも、zod、yup、superstructなどの選択した検証ライブラリを使用することもできます。
publicProcedure.input()
で入力パーサーを定義します。これは、以下に示すように、リゾルバー関数でアクセスできます。
- バニラ
- Zod
- Yup
- Valibot
server/index.tsts
constappRouter =router ({// ...userById :publicProcedure // The input is unknown at this time. A client could have sent// us anything so we won't assume a certain data type..input ((val : unknown) => {// If the value is of type string, return it.// It will now be inferred as a string.if (typeofval === 'string') returnval ;// Uh oh, looks like that input wasn't a string.// We will throw an error instead of running the procedure.throw newError (`Invalid input: ${typeofval }`);}).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server/index.tsts
constappRouter =router ({// ...userById :publicProcedure // The input is unknown at this time. A client could have sent// us anything so we won't assume a certain data type..input ((val : unknown) => {// If the value is of type string, return it.// It will now be inferred as a string.if (typeofval === 'string') returnval ;// Uh oh, looks like that input wasn't a string.// We will throw an error instead of running the procedure.throw newError (`Invalid input: ${typeofval }`);}).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
z.string()
やz.object()
などの任意のZodType
を指定できます。server.tsts
import {z } from 'zod';constappRouter =router ({// ...userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tsts
import {z } from 'zod';constappRouter =router ({// ...userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
yup.string()
やyup.object()
などの任意のYupSchema
を指定できます。server.tsts
import * asyup from 'yup';constappRouter =router ({// ...userById :publicProcedure .input (yup .string ().required ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tsts
import * asyup from 'yup';constappRouter =router ({// ...userById :publicProcedure .input (yup .string ().required ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
wrap
するだけで済みます。server.tsts
import {wrap } from '@typeschema/valibot';import {string } from 'valibot';constappRouter =router ({// ...userById :publicProcedure .input (wrap (string ())).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tsts
import {wrap } from '@typeschema/valibot';import {string } from 'valibot';constappRouter =router ({// ...userById :publicProcedure .input (wrap (string ())).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
このドキュメントの残りの部分では、検証ライブラリとしてzod
を使用します。
4. ミューテーションプロシージャの追加
GraphQLと同様に、tRPCはクエリプロシージャとミューテーションプロシージャを区別します。
プロシージャがサーバー上で機能する方法は、クエリとミューテーションの間でそれほど変わりません。メソッド名が異なり、クライアントがこのプロシージャを使用する方法が変わりますが、それ以外はすべて同じです。
ルーターオブジェクトの新しいプロパティとして追加することにより、userCreate
ミューテーションを追加しましょう
server.tsts
constappRouter =router ({// ...userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;// Create a new user in the databaseconstuser = awaitdb .user .create (input );returnuser ;}),});
server.tsts
constappRouter =router ({// ...userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;// Create a new user in the databaseconstuser = awaitdb .user .create (input );returnuser ;}),});
APIの提供
ルーターを定義したので、それを提供できます。tRPCには多くのアダプターがあるため、選択した任意のバックエンドフレームワークを使用できます。シンプルにするために、standalone
アダプターを使用します。
server/index.tsts
import {createHTTPServer } from '@trpc/server/adapters/standalone';constappRouter =router ({// ...});constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
server/index.tsts
import {createHTTPServer } from '@trpc/server/adapters/standalone';constappRouter =router ({// ...});constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
完全なバックエンドコードを見る
server/db.tsts
typeUser = {id : string;name : string };// Imaginary databaseconstusers :User [] = [];export constdb = {user : {findMany : async () =>users ,findById : async (id : string) =>users .find ((user ) =>user .id ===id ),create : async (data : {name : string }) => {constuser = {id :String (users .length + 1), ...data };users .push (user );returnuser ;},},};
server/db.tsts
typeUser = {id : string;name : string };// Imaginary databaseconstusers :User [] = [];export constdb = {user : {findMany : async () =>users ,findById : async (id : string) =>users .find ((user ) =>user .id ===id ),create : async (data : {name : string }) => {constuser = {id :String (users .length + 1), ...data };users .push (user );returnuser ;},},};
server/trpc.tsts
import {initTRPC } from '@trpc/server';constt =initTRPC .create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/trpc.tsts
import {initTRPC } from '@trpc/server';constt =initTRPC .create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/index.tsts
import {createHTTPServer } from "@trpc/server/adapters/standalone";import {z } from "zod";import {db } from "./db";import {publicProcedure ,router } from "./trpc";constappRouter =router ({userList :publicProcedure .query (async () => {constusers = awaitdb .user .findMany ();returnusers ;}),userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .findById (input );returnuser ;}),userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .create (input );returnuser ;}),});export typeAppRouter = typeofappRouter ;constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
server/index.tsts
import {createHTTPServer } from "@trpc/server/adapters/standalone";import {z } from "zod";import {db } from "./db";import {publicProcedure ,router } from "./trpc";constappRouter =router ({userList :publicProcedure .query (async () => {constusers = awaitdb .user .findMany ();returnusers ;}),userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .findById (input );returnuser ;}),userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .create (input );returnuser ;}),});export typeAppRouter = typeofappRouter ;constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
クライアントで新しいバックエンドを使用する
次に、クライアント側のコードに移動して、エンドツーエンドのタイプセーフの力を活用しましょう。クライアントが使用するためにAppRouter
タイプをインポートすると、実装の詳細をクライアントに漏らすことなく、システムの完全なタイプセーフを実現しました。
1. tRPCクライアントのセットアップ
client/index.tsts
import {createTRPCClient ,httpBatchLink } from '@trpc/client';import type {AppRouter } from './server';// 👆 **type-only** import// Pass AppRouter as generic here. 👇 This lets the `trpc` object know// what procedures are available on the server and their input/output types.consttrpc =createTRPCClient <AppRouter >({links : [httpBatchLink ({url : 'http://localhost:3000',}),],});
client/index.tsts
import {createTRPCClient ,httpBatchLink } from '@trpc/client';import type {AppRouter } from './server';// 👆 **type-only** import// Pass AppRouter as generic here. 👇 This lets the `trpc` object know// what procedures are available on the server and their input/output types.consttrpc =createTRPCClient <AppRouter >({links : [httpBatchLink ({url : 'http://localhost:3000',}),],});
tRPCのリンクはGraphQLのリンクに似ており、サーバーに送信される前にデータフローを制御できます。上記の例では、httpBatchLinkを使用しています。これにより、複数の呼び出しが自動的に1つのHTTPリクエストにバッチ処理されます。リンクの詳細な使用法については、リンクのドキュメントを参照してください。
2. クエリとミューテーション
これで、trpc
オブジェクトでAPIプロシージャにアクセスできます。試してみてください!
client/index.tsts
// Inferred typesconstuser = awaittrpc .userById .query ('1');constcreatedUser = awaittrpc .userCreate .mutate ({name : 'sachinraja' });
client/index.tsts
// Inferred typesconstuser = awaittrpc .userById .query ('1');constcreatedUser = awaittrpc .userCreate .mutate ({name : 'sachinraja' });
完全なオートコンプリート
Intellisenseを開いて、フロントエンドでAPIを調べることができます。すべてのプロシージャルートと、それらを呼び出すためのメソッドが用意されています。
client/index.tsts
// Full autocompletion on your routestrpc .u ;
client/index.tsts
// Full autocompletion on your routestrpc .u ;
自分で試してみてください!
次のステップ
お気に入りのフレームワークでtRPCがどのようにインストールされているかを知るために、サンプルアプリを確認することを強くお勧めします。
デフォルトでは、tRPCはDate
のような複雑な型をJSON同等物(Date
の場合はstring
)にマップします。これらの型の整合性を維持したい場合は、これらのサポートを追加する最も簡単な方法は、superjsonをデータトランスフォーマーとして使用することです。
tRPCには、ReactプロジェクトおよびNext.js用に設計された、より高度なクライアント側ツールが含まれています。