@trpc/server

The tRPC server library

server-setup

core
379 linesSource

Initialize tRPC with initTRPC.create(), define routers with t.router(), create procedures with .query()/.mutation()/.subscription(), configure context with createContext(), export AppRouter type, merge routers with t.mergeRouters(), lazy-load routers with lazy().

tRPC -- Server Setup

Setup

ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;
ts
// server/appRouter.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';

type User = { id: string; name: string };

export const appRouter = router({
  userList: publicProcedure.query(async (): Promise<User[]> => {
    return [{ id: '1', name: 'Katt' }];
  }),
  userById: publicProcedure
    .input(z.string())
    .query(async ({ input }): Promise<User> => {
      return { id: input, name: 'Katt' };
    }),
  userCreate: publicProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async ({ input }): Promise<User> => {
      return { id: '1', ...input };
    }),
});

export type AppRouter = typeof appRouter;
ts
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';

const server = createHTTPServer({ router: appRouter });
server.listen(3000);

Core Patterns

Context with typed session

ts
// server/context.ts
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';

export async function createContext(opts: CreateHTTPContextOptions) {
  const token = opts.req.headers['authorization'];
  return { token };
}

export type Context = Awaited<ReturnType<typeof createContext>>;
ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import type { Context } from './context';

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;
ts
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './context';

const server = createHTTPServer({
  router: appRouter,
  createContext,
});
server.listen(3000);

Inner/outer context split for testability

ts
// server/context.ts
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
import { db } from './db';

interface CreateInnerContextOptions {
  session: { user: { email: string } } | null;
}

export async function createContextInner(opts?: CreateInnerContextOptions) {
  return {
    db,
    session: opts?.session ?? null,
  };
}

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

Infer Context from createContextInner so server-side callers and tests never need HTTP request objects.

Merging child routers

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

export const userRouter = router({
  list: publicProcedure.query(() => []),
});
ts
// server/routers/post.ts
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';

export const postRouter = router({
  create: publicProcedure
    .input(z.object({ title: z.string() }))
    .mutation(({ input }) => ({ id: '1', ...input })),
  list: publicProcedure.query(() => []),
});
ts
// server/routers/_app.ts
import { router } from '../trpc';
import { postRouter } from './post';
import { userRouter } from './user';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

export type AppRouter = typeof appRouter;

Lazy-loaded routers for serverless cold starts

ts
// server/routers/_app.ts
import { lazy } from '@trpc/server';
import { router } from '../trpc';

export const appRouter = router({
  // Short-hand when the module has exactly one router exported
  greeting: lazy(() => import('./greeting.js')),
  // Use .then() to pick a named export when the module exports multiple routers
  user: lazy(() => import('./user.js').then((m) => m.userRouter)),
});

export type AppRouter = typeof appRouter;

Common Mistakes

[CRITICAL] Calling initTRPC.create() more than once

Wrong:

ts
// file: userRouter.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const userRouter = t.router({});

// file: postRouter.ts
import { initTRPC } from '@trpc/server';
const t2 = initTRPC.create();
export const postRouter = t2.router({});

Correct:

ts
// file: trpc.ts (single file, created once)
import { initTRPC } from '@trpc/server';
import type { Context } from './context';

const t = initTRPC.context<Context>().create();

export const router = t.router;
export const publicProcedure = t.procedure;

Multiple tRPC instances cause type mismatches and runtime errors when routers from different instances are merged.

Source: www/docs/server/routers.md

[HIGH] Using reserved words as procedure names

Wrong:

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

const appRouter = router({
  then: publicProcedure.query(() => 'hello'),
});

Correct:

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

const appRouter = router({
  next: publicProcedure.query(() => 'hello'),
});

Router creation throws if procedure names are "then", "call", or "apply" because these conflict with JavaScript Proxy internals.

Source: packages/server/src/unstable-core-do-not-import/router.ts

[CRITICAL] Importing AppRouter as a value import

Wrong:

ts
// client.ts
import { AppRouter } from '../server/router';

Correct:

ts
// client.ts
import type { AppRouter } from '../server/router';

A non-type import pulls the entire server bundle into the client; use import type so it is stripped at build time.

Source: www/docs/server/routers.md

[MEDIUM] Creating context without inner/outer split

Wrong:

ts
import type { CreateExpressContextOptions } from '@trpc/server/adapters/express';

export function createContext({ req }: CreateExpressContextOptions) {
  return { db: prisma, user: getUserFromReq(req) };
}

Correct:

ts
import type { CreateExpressContextOptions } from '@trpc/server/adapters/express';

export function createContextInner(opts: { user?: User }) {
  return { db: prisma, user: opts.user ?? null };
}

export function createContext({ req }: CreateExpressContextOptions) {
  return createContextInner({ user: getUserFromReq(req) });
}

Without an inner context factory, server-side callers and tests must construct HTTP request objects to get context.

Source: www/docs/server/context.md

[HIGH] Merging routers with different transformers

Wrong:

ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';

const t1 = initTRPC.create({ transformer: superjson });
const t2 = initTRPC.create();

const router1 = t1.router({ a: t1.procedure.query(() => 'a') });
const router2 = t2.router({ b: t2.procedure.query(() => 'b') });

t1.mergeRouters(router1, router2);

Correct:

ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';

const t = initTRPC.create({ transformer: superjson });

const router1 = t.router({ a: t.procedure.query(() => 'a') });
const router2 = t.router({ b: t.procedure.query(() => 'b') });

t.mergeRouters(router1, router2);

t.mergeRouters() throws at runtime if the routers were created with different transformer or errorFormatter configurations.

Source: packages/server/src/unstable-core-do-not-import/router.ts

[CRITICAL] Importing appRouter value into client code

Wrong:

ts
// client.ts
import { appRouter } from '../server/router';

type AppRouter = typeof appRouter;

Correct:

ts
// client.ts
import type { AppRouter } from '../server/router';

// server/router.ts
export type AppRouter = typeof appRouter;

Importing the appRouter value bundles the entire server into the client, even if you only use typeof.

Source: www/docs/server/routers.md

See Also

  • middlewares -- add auth, logging, context extension to procedures
  • validators -- add input/output validation with Zod
  • error-handling -- throw and format typed errors
  • server-side-calls -- call procedures from server code
  • adapter-standalone -- mount on Node.js HTTP server
  • adapter-fetch -- mount on edge runtimes