mrkeyoor.com_
Sat 19 Sept 08:55 UTC
npmWeb Backendupdated 19 Sept 2026

@trpc/server review

@trpc/server 11.18.0 defines typed RPC procedures, routers, middleware, errors, context, subscriptions, callers, and HTTP adapters for TypeScript applications. A client imports the AppRouter type and infers inputs and outputs without generated code or a wire schema. Runtime validation is optional and supplied by a compatible validator such as Zod. The server package itself declares 0 direct dependencies and one TypeScript peer. Release 11.18.0 adds server-URL support to the repository's OpenAPI work and a TanStack mutation-option prefix feature; its notes do not announce a core server procedure change.

Verdict

@trpc/server 11.18.0 installed in 4.5 seconds with 0 direct dependencies and 0 audit findings in our sandbox; the full browser import measured 6 KB gzipped. It is a strong fit for one TypeScript-owned client/server system, and a poor contract for public or independently deployed non-TypeScript consumers.

We installed it

Lab card: what happened when we installed @trpc/serverScreenshot of @trpc/server documentation
Install✓ · 4.5s3 packages on disk · 34 MB
ImportESM import works · require() works · ESM package with exports map
Browser6 KBgzipped (16.7 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does @trpc/server install cleanly?

Yes. In a fresh container with an empty cache, npm install @trpc/server finished in 5 seconds, leaving 3 packages and 34 MB on disk. npm audit reported no known vulnerabilities.

How much does @trpc/server add to a browser bundle?

6 KB gzipped (16.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does @trpc/server work with both ESM and CommonJS?

Yes. Both import '@trpc/server' and require('@trpc/server') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does @trpc/server include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

@trpc/server or @orpc/server: which should you use?

@orpc/server: Use it when tRPC-like TypeScript inference plus first-class OpenAPI interoperability fits the API. @trpc/server 11.18.0 installed in 4.5 seconds with 0 direct dependencies and 0 audit findings in our sandbox; the full browser import measured 6 KB gzipped.

When should you not use @trpc/server?

The API must serve mobile, partner, or non-TypeScript consumers; the shared compile-time type is not a portable runtime contract

API stability4/5Within v11, initTRPC, procedures, routers, middleware, TRPCError, createCallerFactory, and the supported adapter entries have remained recognizable across frequent minor releases. The v10 to v11 migration did rename client APIs and changed subscription and transformer guidance, so old examples still carry migration cost. Release 11.18.0 does not list a breaking server procedure change in its notes.
Docs4/5The versioned site covers procedures, validators, context, authorization, middleware, error formatting, server-side callers, data transformers, batching, subscriptions, and each official adapter with runnable examples. Framework recipes are extensive. Search can still surface v10 pages and older createTRPCProxyClient or subscription guidance, so confirm the v11 selector and package versions before applying a snippet.
Maintenance5/5GitHub reports 40,543 stars, 281 open issues and pull requests, an unarchived repository, and a push on 2026-08-13. Version 11.18.0 shipped on 2026-06-18, and its release notes include OpenAPI server URL support, TanStack mutation option prefix work, CI repairs, and documentation cleanup. The active monorepo covers multiple packages, so not every release item changes @trpc/server itself.
Ecosystem4/5The npm downloads endpoint counted 5,663,544 downloads for the latest completed week. The exports map includes Fetch, Next.js, Express, Fastify, Node HTTP, AWS Lambda, standalone, and WebSocket adapters, while client packages integrate with TanStack Query. That breadth is strong inside TypeScript. Outside it, there is no mandatory language-neutral schema or generated SDK, which limits public API interoperability.

Use it if

  • Client and server are TypeScript codebases that can share the AppRouter type from one versioned source
  • Internal APIs need typed queries, mutations, middleware, errors, batching, and subscriptions without code generation
  • A supported Fetch, Next.js, Express, Fastify, Node HTTP, Lambda, standalone, or WebSocket adapter fits the deployment
  • The team will add explicit input and output validators where data crosses the network
Skip it if

Setup reality

We installed @trpc/server 11.18.0 in a fresh unprivileged Node 22 Bookworm container. npm completed in 4.5 seconds and left 3 packages using 34 MB on disk. The server package itself was 2,944 KB unpacked, declared 0 direct dependencies and 1 peer dependency, and used the MIT license. npm audit reported 0 known vulnerabilities. TypeScript >=5.7.2 is the peer expected by this release.

The package is ESM with an exports map that supplies import and require branches. Both loading styles worked in our sandbox, and TypeScript declarations are bundled. A full esbuild browser import measured 16.7 KB minified and 6 KB gzipped. Application clients should import only the AppRouter type from server code; importing router values can pull database, secrets, or adapter modules into a browser graph.

A real service needs more than this one package. Add a runtime input validator, the matching HTTP adapter entry, and @trpc/client or a framework integration on the consumer side. createContext() runs per request and owns authentication, tenant lookup, and request-scoped resources. Middleware can narrow context after a check, but authorization must still match each procedure's resource rather than stopping at a signed-in boolean.

The Fetch adapter needs the exact endpoint used by the client and both request methods required by the chosen link. Express, Fastify, and Fetch adapters expose different request objects, so keep transport-specific context extraction at the edge. There is no generated or negotiated schema protecting separate deployments. Publish the router type through a versioned package, deploy compatible client and server releases, and add contract-level integration tests for version skew.

Patterns

Create a validated query define-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().min(1) }))
    .query(({ input }) => ({ text: 'Hello ' + input.name })),
});
export type AppRouter = typeof appRouter;

Export AppRouter as a type to clients. Keep the appRouter value in server-only modules.

Type per-request context typed-context

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

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

Context is created for each request by the adapter. Put request-scoped identity and resources here, not global mutable state.

Narrow context after authentication auth-middleware

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

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

const protectedProcedure = t.procedure.use(requireUser);

Passing the narrowed user through next() makes it non-null in downstream procedures. Resource-level authorization still belongs in the procedure.

Validate mutation input validated-mutation

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

The validator runs before the resolver and supplies the input type. Without input(), network values are not validated by TypeScript.

Restrict fields returned by a procedure validate-output

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

const me = protectedProcedure
  .output(publicUser)
  .query(({ ctx }) => ctx.user);

output() checks the result before transport. It helps stop an ORM record with private columns from leaving the server.

Serve a router through Fetch fetch-adapter

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';

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

export { handler as GET, handler as POST };

The endpoint must match the client link URL. Export the methods used by that link, including POST for batched calls.

Mount the Express middleware express-adapter

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

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

Express context receives Node request and response objects rather than a Fetch Request. Keep adapter-specific extraction in this boundary.

Call procedures without HTTP server-caller

const createCaller = t.createCallerFactory(appRouter);
const caller = createCaller({ user, db });
const result = await caller.greet({ name: 'Ada' });

createCallerFactory executes the same procedures and middleware in-process. Use it for server composition and tests, not as a replacement for ordinary function calls everywhere.

Compose domain routers merge-routers

const userRouter = t.router({ me });
const postRouter = t.router({ create: createPost });

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

Nested routers preserve procedure paths such as user.me and post.create. They improve ownership but do not eliminate TypeScript inference work.

Return a client-safe error code typed-error

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

TRPCError maps known codes into the protocol response. Unexpected Errors should remain generic for clients and be logged at the adapter boundary.

Add typed validation metadata error-formatter

import { ZodError } from 'zod';

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        validation: error.cause instanceof ZodError
          ? error.cause.flatten()
          : null,
      },
    };
  },
});

errorFormatter changes every error shape and its inferred client type. Expose only serializable details that are safe for callers.

Yield subscription events subscription

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

An async generator can observe the abort signal and stop work after disconnect. The client still needs a subscription-capable link and matching transport.

Alternatives

PackageRegistryPick it when
@orpc/servernpmUse it when tRPC-like TypeScript inference plus first-class OpenAPI interoperability fits the API
@ts-rest/corenpmUse it for a shared TypeScript REST contract with explicit methods, paths, statuses, and schemas
hononpmUse it for a small web framework with typed routes and an RPC client option rather than a procedure router
graphql-yoganpmUse it when a language-neutral GraphQL schema and broader client ecosystem are required

More web backend guides

urllib3 · requests · ws · anyio · undici · httpx · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.