メインコンテンツへスキップ
バージョン: 11.x

プロシージャの定義

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

プロシージャはクライアントに公開される関数であり、以下のいずれかの種類があります:

  • Query - データ取得に使用され、通常はデータを変更しません

  • Mutation - データ送信に使用され、作成・更新・削除目的で頻繁に利用されます

  • Subscription - 必要性が低い場合があり、専用ドキュメントを用意しています

tRPCにおけるプロシージャは、バックエンド関数を作成するための非常に柔軟なプリミティブです。イミュータブルなビルダーパターンを採用しているため、再利用可能なベースプロシージャを作成して複数のプロシージャ間で機能を共有できます。

プロシージャの作成

tRPCセットアップ時に作成するtオブジェクトは初期t.procedureを返し、これが他のすべてのプロシージャの基盤となります:

ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.context<{ signGuestBook: () => Promise<void> }>().create();
 
export const router = t.router;
export const publicProcedure = t.procedure;
 
const appRouter = router({
// Queries are the best place to fetch data
hello: publicProcedure.query(() => {
return {
message: 'hello world',
};
}),
 
// Mutations are the best place to do things like updating a database
goodbye: publicProcedure.mutation(async (opts) => {
await opts.ctx.signGuestBook();
 
return {
message: 'goodbye!',
};
}),
});
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.context<{ signGuestBook: () => Promise<void> }>().create();
 
export const router = t.router;
export const publicProcedure = t.procedure;
 
const appRouter = router({
// Queries are the best place to fetch data
hello: publicProcedure.query(() => {
return {
message: 'hello world',
};
}),
 
// Mutations are the best place to do things like updating a database
goodbye: publicProcedure.mutation(async (opts) => {
await opts.ctx.signGuestBook();
 
return {
message: 'goodbye!',
};
}),
});

再利用可能な「ベースプロシージャ」

一般的なパターンとして、t.procedurepublicProcedureとしてリネーム・エクスポートすることを推奨します。これにより、特定ユースケース向けに他の名前付きプロシージャを作成・エクスポートする余地が生まれます。この「ベースプロシージャ」パターンはtRPCにおけるコードと動作の再利用の中核であり、ほぼすべてのアプリケーションで必要となるでしょう。

以下のコードでは、再利用可能なベースプロシージャを使用してアプリの共通ユースケースを構築しています。ログイン済みユーザー向けのベースプロシージャ(authedProcedure)と、organizationIdを受け取りユーザーがその組織に所属していることを検証する別のベースプロシージャを作成しています。

これは簡略化した例です。実際にはHeadersContextMiddlewareMetadataを組み合わせて、ユーザーの認証認可を行う必要があるでしょう。

ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
 
type Organization = {
id: string;
name: string;
};
type Membership = {
role: 'ADMIN' | 'MEMBER';
Organization: Organization;
};
type User = {
id: string;
memberships: Membership[];
};
type Context = {
/**
* User is nullable
*/
user: User | null;
};
 
const t = initTRPC.context<Context>().create();
 
export const publicProcedure = t.procedure;
 
// procedure that asserts that the user is logged in
export const authedProcedure = t.procedure.use(async function isAuthed(opts) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
(property) user: User | null
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
 
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
},
});
});
 
// procedure that a user is a member of a specific organization
export const organizationProcedure = authedProcedure
.input(z.object({ organizationId: z.string() }))
.use(function isMemberOfOrganization(opts) {
const membership = opts.ctx.user.memberships.find(
(m) => m.Organization.id === opts.input.organizationId,
);
if (!membership) {
throw new TRPCError({
code: 'FORBIDDEN',
});
}
return opts.next({
ctx: {
Organization: membership.Organization,
},
});
});
 
export const appRouter = t.router({
whoami: authedProcedure.query(async (opts) => {
// user is non-nullable here
const { ctx } = opts;
const ctx: { user: User; }
return ctx.user;
}),
addMember: organizationProcedure
.input(
z.object({
email: z.string().email(),
}),
)
.mutation((opts) => {
// ctx contains the non-nullable user & the organization being queried
const { ctx } = opts;
const ctx: { user: User; Organization: Organization; }
 
// input includes the validated email of the user being invited & the validated organizationId
const { input } = opts;
const input: { organizationId: string; email: string; }
 
return '...';
}),
});
ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
 
type Organization = {
id: string;
name: string;
};
type Membership = {
role: 'ADMIN' | 'MEMBER';
Organization: Organization;
};
type User = {
id: string;
memberships: Membership[];
};
type Context = {
/**
* User is nullable
*/
user: User | null;
};
 
const t = initTRPC.context<Context>().create();
 
export const publicProcedure = t.procedure;
 
// procedure that asserts that the user is logged in
export const authedProcedure = t.procedure.use(async function isAuthed(opts) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
(property) user: User | null
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
 
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
},
});
});
 
// procedure that a user is a member of a specific organization
export const organizationProcedure = authedProcedure
.input(z.object({ organizationId: z.string() }))
.use(function isMemberOfOrganization(opts) {
const membership = opts.ctx.user.memberships.find(
(m) => m.Organization.id === opts.input.organizationId,
);
if (!membership) {
throw new TRPCError({
code: 'FORBIDDEN',
});
}
return opts.next({
ctx: {
Organization: membership.Organization,
},
});
});
 
export const appRouter = t.router({
whoami: authedProcedure.query(async (opts) => {
// user is non-nullable here
const { ctx } = opts;
const ctx: { user: User; }
return ctx.user;
}),
addMember: organizationProcedure
.input(
z.object({
email: z.string().email(),
}),
)
.mutation((opts) => {
// ctx contains the non-nullable user & the organization being queried
const { ctx } = opts;
const ctx: { user: User; Organization: Organization; }
 
// input includes the validated email of the user being invited & the validated organizationId
const { input } = opts;
const input: { organizationId: string; email: string; }
 
return '...';
}),
});

「ベースプロシージャ」のオプション型推論

プロシージャの入力/出力型を推論することに加え、inferProcedureBuilderResolverOptionsを使用して特定のプロシージャビルダー(またはベースプロシージャ)のオプション型を推論できます。

この型ヘルパーは関数パラメータの型宣言に有用です。例:プロシージャのハンドラー(主要実行コード)をルーター定義から分離する場合、または複数プロシージャで動作するヘルパー関数を作成する場合など。

ts
async function getMembersOfOrganization(
opts: inferProcedureBuilderResolverOptions<typeof organizationProcedure>,
) {
// input and ctx are now correctly typed!
const { ctx, input } = opts;
 
return await prisma.user.findMany({
where: {
membership: {
organizationId: ctx.Organization.id,
},
},
});
}
export const appRouter = t.router({
listMembers: organizationProcedure.query(async (opts) => {
// use helper function!
const members = await getMembersOfOrganization(opts);
 
return members;
}),
});
ts
async function getMembersOfOrganization(
opts: inferProcedureBuilderResolverOptions<typeof organizationProcedure>,
) {
// input and ctx are now correctly typed!
const { ctx, input } = opts;
 
return await prisma.user.findMany({
where: {
membership: {
organizationId: ctx.Organization.id,
},
},
});
}
export const appRouter = t.router({
listMembers: organizationProcedure.query(async (opts) => {
// use helper function!
const members = await getMembersOfOrganization(opts);
 
return members;
}),
});

サブスクリプション

サブスクリプションに関する詳細は、サブスクリプションガイドをご覧ください。