mrkeyoor.com_
Wed 05 Aug 05:03 UTC
npmWeb Backendupdated 05 Aug 2026

@trpc/server

@trpc/server is the server half of tRPC, which gives you end-to-end typesafe APIs in TypeScript without schemas or code generation. You define procedures (queries, mutations, subscriptions) in a router; the client imports only the router's type and gets compile-time checking and autocomplete on inputs, outputs, and errors. There is no codegen step and no runtime schema on the wire, and the server package itself has zero dependencies.

Verdict

The best developer experience available for TypeScript-only, same-repo APIs; batching, middleware, and typed errors come along free. The moment a non-TypeScript consumer appears its core advantage evaporates, so choose based on who will call the API.

API stability4/5v11 is the current line with frequent minor releases (11.18 now) and no breaking churn within the major; the v10 to v11 jump did rename core client APIs, and old content still circulates.
Docs4/5trpc.io covers concepts, every adapter, and framework integrations with example apps; versioned docs help, but search results still land on outdated v10 pages often enough to cost time.
Maintenance4/5Pushed July 2026 with three named project leads and steady releases; 260 open issues and PRs on a 40k-star project is normal, but the bus factor is thinner than the download count suggests.
Ecosystem4/5Official adapters for Next, Express, Fastify, and the Fetch API, a community add-on list, and deep TanStack Query integration; everything stops at the TypeScript border by design.

Use it if

  • Your client and server are both TypeScript in the same repo or monorepo and you own both ends
  • You are tired of hand-written fetch wrappers and API types drifting out of sync with the backend
  • You want middleware, auth context, and input validation with types flowing through automatically
  • You are on Next.js or another full-stack TS setup where the official adapters (Next, Express, Fastify, Fetch) drop in
Skip it if

Setup reality

It is a family of packages, not one install: @trpc/server plus a validator (usually zod) on the back end, @trpc/client and typically the TanStack Query integration on the front end. First-time wiring of the context function, an adapter, and the shared AppRouter type export takes a real afternoon. The v10 to v11 migration renamed client entry points, so tutorials written before 2024 will steer you into APIs that no longer exist, and editor slowness on big routers is a known, documented cost you design around by splitting routers.

Patterns

Create a router with a typed proceduredefine-router

import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  greet: t.procedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => `Hello ${input.name}`),
});

export type AppRouter = typeof appRouter;

Export only the type to the client; importing the router value client-side drags server code into your bundle.

Build per-request context from headersrequest-context

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

export async function createContext({ req }: { req: Request }) {
  const user = await getUserFromToken(req.headers.get('authorization'));
  return { user };
}

type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();

Call initTRPC.context<Context>() before .create(); adding context later means retyping every procedure.

Protect procedures with middlewareauth-middleware

import { TRPCError } from '@trpc/server';

const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { user: ctx.user } });
});

export const protectedProcedure = t.procedure.use(isAuthed);

Returning next() with a narrowed ctx is what makes ctx.user non-nullable in every procedure downstream; skip it and you null-check forever.

Validate a mutation input with zodmutation-input

const appRouter = t.router({
  createPost: protectedProcedure
    .input(z.object({ title: z.string().min(1), body: z.string() }))
    .mutation(async ({ input, ctx }) => {
      return db.post.create({ data: { ...input, authorId: ctx.user.id } });
    }),
});

Invalid input rejects with a BAD_REQUEST error before your handler runs; the input type in the handler is inferred from the schema.

Serve the router from a Fetch-based runtimefetch-adapter

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from './router';
import { createContext } from './context';

const handler = (req: Request) =>
  fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext });

export { handler as GET, handler as POST };

This one adapter covers Next.js route handlers, Bun, Deno, and edge runtimes; export both GET and POST or batched requests fail.

Mount tRPC inside an Express appexpress-adapter

import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';

const app = express();
app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({ router: appRouter, createContext })
);
app.listen(3000);

The express adapter gets req and res in createContext instead of a Fetch Request, so context code is adapter-specific.

Call the API from a plain TypeScript clientvanilla-client

import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';

const client = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
});

const greeting = await client.greet.query({ name: 'world' });

v11 renamed createTRPCProxyClient to createTRPCClient; httpBatchLink combines parallel calls into a single HTTP request.

Throw typed errors and log them centrallyerror-handling

import { TRPCError } from '@trpc/server';

throw new TRPCError({ code: 'NOT_FOUND', message: 'post does not exist' });

// central logging in the adapter:
fetchRequestHandler({
  endpoint: '/api/trpc', req, router: appRouter, createContext,
  onError({ error, path }) {
    console.error(`tRPC error on ${path}:`, error);
  },
});

Plain thrown Errors surface as INTERNAL_SERVER_ERROR with the message hidden in production; use TRPCError codes for anything client-facing.

Validate what leaves the serveroutput-validation

const publicUser = z.object({ id: z.string(), name: z.string() });

const appRouter = t.router({
  me: protectedProcedure
    .output(publicUser)
    .query(({ ctx }) => ctx.user),
});

output() strips or rejects extra fields, which is cheap insurance when handlers return raw database rows with password hashes attached.

Stream events with an async generator subscriptionsse-subscription

const appRouter = t.router({
  onTick: t.procedure.subscription(async function* (opts) {
    let i = 0;
    while (!opts.signal?.aborted) {
      yield i++;
      await new Promise((r) => setTimeout(r, 1000));
    }
  }),
});

v11 subscriptions run over Server-Sent Events with httpSubscriptionLink on the client, so WebSockets are no longer required for streaming.

Alternatives

PackageRegistryPick it when
@orpc/servernpmYou want tRPC-style typesafety but with OpenAPI output for non-TS consumers built in.
@ts-rest/corenpmYou prefer a contract-first REST approach that still shares types between client and server.
hononpmYou want a plain typed web framework with an RPC client mode and fewer layers than the tRPC stack.