@trpc/server
Version:
173 lines (138 loc) • 5.85 kB
Markdown
From https://trpc.io/docs/server/procedures
A procedure is a function which is exposed to the client, it can be one of:
- a `Query` - used to fetch data, generally does not change any data
- a `Mutation` - used to send data, often for create/update/delete purposes
- a `Subscription` - you might not need this, and we have [dedicated documentation](https://trpc.io/docs/server/subscriptions)
Procedures in tRPC are very flexible primitives to create backend functions. They use an immutable builder pattern, which means you can [create reusable base procedures](https://trpc.io/docs/server/procedures#reusable-base-procedures) that share functionality among multiple procedures.
The `t` object you create during tRPC setup returns an initial `t.procedure` which all other procedures are built on:
```ts no-check
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!',
};
}),
});
```
As a general pattern we recommend you rename and export `t.procedure` as `publicProcedure`, which then makes room for you to create other named procedures for specific use cases and export those too. This pattern is called "base procedures" and is a key pattern for code and behaviour re-use in tRPC; every application is likely to need it.
In the below code, we're using reusable base procedures to build common use-cases for our app - we're making a reusable base procedures for logged in users (`authedProcedure`) & another base procedure that takes an `organizationId` and validates that a user is part of that organization.
> This is a simplified example; in practice you may want to use some combination of [Headers](https://trpc.io/docs/client/headers), [Context](https://trpc.io/docs/server/context), [Middleware](https://trpc.io/docs/server/middlewares), and [Metadata](https://trpc.io/docs/server/metadata), to [authenticate](https://en.wikipedia.org/wiki/Authentication) and [authorize](https://trpc.io/docs/server/authorization) your users.
```ts no-check
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) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
},
});
});
// procedure that asserts 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;
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;
// input includes the validated email of the user being invited & the validated organizationId
const { input } = opts;
return '...';
}),
});
```
In addition to being able to [infer the input and output types](https://trpc.io/docs/client/vanilla/infer-types#inferring-input--output-types) of a procedure, you can also infer the options type of a specific procedure builder (or base procedure) using `inferProcedureBuilderResolverOptions`.
This type helper is useful for declaring a type to a function's parameters. Like for example, separating the procedure's handler (main execution code) from its definition at the router, or for creating a helper function that works with multiple procedures.
```ts no-check
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;
}),
});
```
For information on subscriptions, see [our subscriptions guide](https://trpc.io/docs/server/subscriptions).