UNPKG

@trpc/server

Version:

The tRPC server library

481 lines (367 loc) • 16 kB
--- name: server-setup description: Set up a tRPC server. Initialize tRPC once with initTRPC.context<Context>().create(), define routers with t.router() and procedures with .query()/.mutation(), build reusable base procedures such as publicProcedure and authedProcedure, type and create the request context with createContext() (inner/outer context for tests and server-side calls), export the AppRouter type, nest child routers or merge them with t.mergeRouters(), lazy-load routers with lazy(). sources: - www/v11/docs/server/overview.md - www/v11/docs/server/routers.md - www/v11/docs/server/procedures.md - www/v11/docs/server/context.md - www/v11/docs/server/merging-routers.md - www/v11/docs/server/metadata.md metadata: type: core library: trpc library_version: 11.23.0 --- <!-- Generated from www/v11/docs/server/overview.md by www/build/skills.ts. Edit the docs instead. --> # Backend Usage From https://trpc.io/docs/server/overview This section covers everything you need to set up and configure your tRPC backend. You'll learn how to define [routers](https://trpc.io/docs/server/routers) and [procedures](https://trpc.io/docs/server/procedures), set up [context](https://trpc.io/docs/server/context), add [input validation](https://trpc.io/docs/server/validators), and use [middlewares](https://trpc.io/docs/server/middlewares) to extend your API. Once your API is defined, you can host it using one of the available [adapters](https://trpc.io/docs/server/adapters) for platforms like Express, Fastify, Next.js, AWS Lambda, and more. ## Initialize tRPC You should initialize tRPC **exactly once** per application. Multiple instances of tRPC will cause issues: for example, `t.mergeRouters()` throws if the routers come from different instances that each set a [transformer](https://trpc.io/docs/server/data-transformers) or [error formatter](https://trpc.io/docs/server/error-formatting). Create `t` in one file and import your procedures and `router` from it everywhere. ```ts no-check title="server/trpc.ts" import { initTRPC } from '@trpc/server'; // You can use any variable name you like. // We use t to keep things simple. const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; ``` You'll notice we are exporting certain methods of the `t` variable here rather than `t` itself. This is to establish a certain set of procedures that we will use idiomatically in our codebase. From https://trpc.io/docs/server/routers#initialize-trpc ## Defining a router Next, let's define a router with a procedure to use in our application. We have now created an API "endpoint". In order for these endpoints to be exposed to the frontend, your [Adapter](https://trpc.io/docs/server/adapters) should be configured with your `appRouter` instance. ```ts no-check title="server/_app.ts" import { publicProcedure, router } from './trpc'; const appRouter = router({ greeting: publicProcedure.query(() => 'hello tRPC v11!'), }); // Export only the type of a router! // This prevents us from importing server code on the client. export type AppRouter = typeof appRouter; ``` On the client, import it with `import type { AppRouter } from '...'` and never import the `appRouter` value: a type-only import is always erased, so your server code can't end up in the client bundle. Procedures can't be named `then`, `call` or `apply`: `router()` throws if you use these names. From https://trpc.io/docs/server/routers#defining-a-router ## Defining an inline sub-router When you define an inline sub-router, you can represent your router as a plain object. In the below example, `nested1` and `nested2` are equal: ```ts no-check title="server/_app.ts" import * as trpc from '@trpc/server'; import { publicProcedure, router } from './trpc'; const appRouter = router({ // Using the router() method nested1: router({ proc: publicProcedure.query(() => '...'), }), // Using an inline sub-router nested2: { proc: publicProcedure.query(() => '...'), }, }); ``` From https://trpc.io/docs/server/routers#inline-sub-router ## Writing 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!', }; }), }); ``` From https://trpc.io/docs/server/procedures#writing-procedures ## Reusable "Base Procedures" 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 '...'; }), }); ``` From https://trpc.io/docs/server/procedures#reusable-base-procedures ## Defining the context type When initializing tRPC using `initTRPC`, you should pipe `.context<TContext>()` to the `initTRPC` builder function before calling `.create()`. The type `TContext` can either be inferred from a function's return type or be explicitly defined. This will make sure your context is properly typed in your procedures and middlewares. ```ts no-check import { initTRPC } from '@trpc/server'; import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; export const createContext = async (opts: CreateHTTPContextOptions) => { // Example: extract a session token from the request headers const token = opts.req.headers['authorization']; return { token, }; }; export type Context = Awaited<ReturnType<typeof createContext>>; const t = initTRPC.context<Context>().create(); t.procedure.use((opts) => { opts.ctx; return opts.next(); }); ``` From https://trpc.io/docs/server/context#defining-the-context-type ## Creating the context The `createContext()` function must be passed to the handler mounting your appRouter. The handler may use HTTP or a [server-side call](https://trpc.io/docs/server/server-side-calls). `createContext()` is called once per request, so all procedures within a single batched request share the same context. ```ts no-check // 1. HTTP request import { createHTTPHandler } from '@trpc/server/adapters/standalone'; import { createContext } from './context'; import { appRouter } from './router'; const handler = createHTTPHandler({ router: appRouter, createContext, }); ``` ```ts no-check // 2. Server-side call import { createContext } from './context'; import { createCaller } from './router'; const caller = createCaller(await createContext()); ``` ```ts no-check // 3. Server-side helpers (Next.js-specific, see /docs/client/nextjs/pages-router/server-side-helpers) import { createServerSideHelpers } from '@trpc/react-query/server'; import { createContext } from './context'; import { appRouter } from './router'; const helpers = createServerSideHelpers({ router: appRouter, ctx: await createContext(), }); ``` From https://trpc.io/docs/server/context#creating-the-context ## Inner and outer context In some scenarios it could make sense to split up your context into "inner" and "outer" functions. **Inner context** is where you define context which doesn’t depend on the request, e.g. your database connection. You can use this function for integration testing or [server-side calls](https://trpc.io/docs/server/server-side-calls), where you don’t have a request object. Whatever is defined here will **always** be available in your procedures. > [!NOTE] Tradeoff for large clients in `createContextInner` > Putting a database client such as `prisma` on `createContextInner` is convenient and common, but large generated clients (like Prisma) can increase type-checking overhead because they become part of your context type across procedures. > > If that overhead becomes noticeable, an alternative is to keep context smaller and import the client directly at call sites where needed. **Outer context** is where you define context which depends on the request, e.g. for the user's session. Whatever is defined here is only available for procedures that are called via HTTP. ### Example for inner & outer context ```ts no-check import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; import { getSessionFromCookie, type Session } from './auth'; import { db } from './db'; /** * Defines your inner context shape. * Add fields here that the inner context brings. */ interface CreateInnerContextOptions { session: Session | null; } /** * Inner context. Will always be available in your procedures, in contrast to the outer context. * * Also useful for: * - testing, so you don't have to mock `req`/`res` * - server-side calls where we don't have `req`/`res` * * @see https://trpc.io/docs/v11/context#inner-and-outer-context */ export async function createContextInner(opts?: CreateInnerContextOptions) { return { db, session: opts?.session, }; } /** * Outer context. Used in the routers and will e.g. bring `req` & `res` to the context as "not `undefined`". * * @see https://trpc.io/docs/v11/context#inner-and-outer-context */ export async function createContext(opts: CreateHTTPContextOptions) { const session = getSessionFromCookie(opts.req); const contextInner = await createContextInner({ session }); return { ...contextInner, req: opts.req, res: opts.res, }; } export type Context = Awaited<ReturnType<typeof createContextInner>>; // The usage in your router is the same as the example above. ``` It is important to infer your `Context` from the **inner** context, as only what is defined there is really always available in your procedures. If you don't want to check `req` or `res` for `undefined` in your procedures all the time, you could build a small reusable procedure for that: ```ts no-check export const apiProcedure = publicProcedure.use((opts) => { if (!opts.ctx.req || !opts.ctx.res) { throw new Error('You are missing `req` or `res` in your call.'); } return opts.next({ ctx: { // We overwrite the context with the truthy `req` & `res`, which will also overwrite the types used in your procedure. req: opts.ctx.req, res: opts.ctx.res, }, }); }); ``` From https://trpc.io/docs/server/context#inner-and-outer-context ## Merging with child routers ```ts no-check title="routers/user.ts" import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const userRouter = router({ list: publicProcedure.query(() => { // [..] return []; }), }); ``` ```ts no-check title="routers/post.ts" import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const postRouter = router({ create: publicProcedure .input( z.object({ title: z.string(), }), ) .mutation((opts) => { const { input } = opts; // [...] }), list: publicProcedure.query(() => { // ... return []; }), }); ``` ```ts no-check title="routers/_app.ts" import { router } from '../trpc'; import { userRouter } from './user'; import { postRouter } from './post'; const appRouter = router({ user: userRouter, post: postRouter, }); appRouter.user appRouter.post export type AppRouter = typeof appRouter; ``` From https://trpc.io/docs/server/merging-routers#merging-with-child-routers ## Dynamically load routers You can use the `lazy` function to dynamically load your routers. This can be useful to reduce cold starts of your application. There's no difference in how you use the router after it's been lazy loaded vs. how you use a normal router. ```ts no-check title="routers/greeting.ts" import { router, publicProcedure } from '../trpc'; export const greetingRouter = router({ hello: publicProcedure.query(() => 'world'), }); ``` ```ts no-check title="routers/user.ts" import { router, publicProcedure } from '../trpc'; export const userRouter = router({ list: publicProcedure.query(() => ['John', 'Jane', 'Jim']), }); ``` ```ts no-check title="routers/_app.ts" import { lazy } from '@trpc/server'; import { router } from '../trpc'; export const appRouter = router({ // Option 1: Short-hand when the module has exactly 1 router exported greeting: lazy(() => import('./greeting.js')), // Option 2: if exporting more than 1 router user: lazy(() => import('./user.js').then((m) => m.userRouter)), }); export type AppRouter = typeof appRouter; ``` From https://trpc.io/docs/server/merging-routers#lazy-load ## References - [Define Routers](references/server-routers.md) - [Define Procedures](references/server-procedures.md) - [Context](references/server-context.md) - [Merging Routers](references/server-merging-routers.md) - [Metadata](references/server-metadata.md)