@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.
@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
| Install | ✓ · 4.5s | 3 packages on disk · 34 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 6 KB | gzipped (16.7 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 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
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
- The API must serve mobile, partner, or non-TypeScript consumers; the shared compile-time type is not a portable runtime contract
- Client and server deploy independently without a disciplined shared-package release process; AppRouter version skew is discovered at runtime
- You are building a public API whose consumers need stable URLs, language-neutral documentation, and generated clients; procedure names alone do not supply those artifacts
- Large inferred routers already make TypeScript editor and build performance unacceptable; splitting routers helps organization but does not remove compiler work
- You expect input types to validate network data automatically; tRPC infers from the validator you attach and accepts unvalidated input when no parser is configured
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
| Package | Registry | Pick it when |
|---|---|---|
| @orpc/server | npm | Use it when tRPC-like TypeScript inference plus first-class OpenAPI interoperability fits the API |
| @ts-rest/core | npm | Use it for a shared TypeScript REST contract with explicit methods, paths, statuses, and schemas |
| hono | npm | Use it for a small web framework with typed routes and an RPC client option rather than a procedure router |
| graphql-yoga | npm | Use 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.

