tRPC logo

Skill

server-setup

initialize and configure tRPC servers

Published by tRPC Updated Jul 18
Covers Backend TypeScript tRPC API Development

Description

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

SKILL.md

tRPC -- Server Setup

Setup

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

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;
// 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;
// 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

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

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

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

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

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

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

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

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

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

Correct:

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:

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

Correct:

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

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

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

Correct:

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:

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:

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:

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

type AppRouter = typeof appRouter;

Correct:

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

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.