Skip to content

@trpc/server

The tRPC server library

server-setup

481 linesSource

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().

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 and procedures, set up context, add input validation, and use middlewares to extend your API.

Once your API is defined, you can host it using one of the available 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 or error formatter. Create t in one file and import your procedures and router from it everywhere.

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 should be configured with your appRouter instance.

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:

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
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, Context, Middleware, and Metadata, to authenticate and authorize your users.

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) {
    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
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.

createContext() is called once per request, so all procedures within a single batched request share the same context.

ts
// 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
// 2. Server-side call
import { createContext } from './context';
import { createCaller } from './router';

const caller = createCaller(await createContext());
ts
// 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, where you don’t have a request object. Whatever is defined here will always be available in your procedures.

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
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
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

routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';

export const userRouter = router({
  list: publicProcedure.query(() => {
    // [..]
    return [];
  }),
});
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 [];
  }),
});
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.

routers/greeting.ts
import { router, publicProcedure } from '../trpc';

export const greetingRouter = router({
  hello: publicProcedure.query(() => 'world'),
});
routers/user.ts
import { router, publicProcedure } from '../trpc';

export const userRouter = router({
  list: publicProcedure.query(() => ['John', 'Jane', 'Jim']),
});
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