skills/ trpc/trpc

middlewares

Create and compose tRPC middleware with t.procedure.use(), extend context via opts.next({ ctx }), build reusable middleware with .concat() and .unstable_pipe(), define base procedures like publicProcedure and authedProcedure. Access raw input with getRawInput(). Logging, timing, OTEL tracing pattern

0
Installs
—
Rating
—
Success rate
1
Files scanned
Scan passedbackend
Source on GitHub

Security scan

Scan passed

No risky patterns were found in the scanned files.

1 files scannedscanner v1.2.0Oct 11, 2026

Content sha256 4a5c46d1ab9427bf… — run codexguild_scan_skills after installing to verify your local copy.

Static analysis is a first line of defense, not a guarantee. Read the source

SKILL.md

exact scanned copy

tRPC -- Middlewares

Setup

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

type Context = {
  user?: { id: string; isAdmin: boolean };
};

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

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

Core Patterns

Auth middleware that narrows context type

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

type Context = {
  user?: { id: string; isAdmin: boolean };
};

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

export const publicProcedure = t.procedure;

export const authedProcedure = t.procedure.use(async (opts) => {
  const { ctx } = opts;
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return opts.next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const adminProcedure = t.procedure.use(async (opts) => {
  const { ctx } = opts;
  if (!ctx.user?.isAdmin) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return opts.next({
    ctx: {
      user: ctx.user,
    },
  });
});

After the middleware, ctx.user is non-nullable in downstream procedures.

Logging and timing middleware

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

const t = initTRPC.create();

export const loggedProcedure = t.procedure.use(async (opts) => {
  const start = Date.now();

  const result = await opts.next();

  const durationMs = Date.now() - start;
  const meta = { path: opts.path, type: opts.type, durationMs };

  result.ok
    ? console.log('OK request timing:', meta)
    : console.error('Non-OK request timing', meta);

  return result;
});

Reusable middleware with .concat()

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

export function createMyPlugin() {
  const t = initTRPC.context<{}>().meta<{}>().create();

  return {
    pluginProc: t.procedure.use((opts) => {
      return opts.next({
        ctx: {
          fromPlugin: 'hello from myPlugin' as const,
        },
      });
    }),
  };
}
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { createMyPlugin } from './myPlugin';

const t = initTRPC.context<{}>().create();
const plugin = createMyPlugin();

export const publicProcedure = t.procedure;

export const procedureWithPlugin = publicProcedure.concat(plugin.pluginProc);

.concat() merges a partial procedure (from any tRPC instance) into your procedure chain, as long as context and meta types overlap.

Extending middlewares with .unstable_pipe()

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

const t = initTRPC.create();

const fooMiddleware = t.middleware((opts) => {
  return opts.next({
    ctx: { foo: 'foo' as const },
  });
});

const barMiddleware = fooMiddleware.unstable_pipe((opts) => {
  console.log(opts.ctx.foo);
  return opts.next({
    ctx: { bar: 'bar' as const },
  });
});

const barProcedure = t.procedure.use(barMiddleware);

Piped middlewares run in order and each receives the context from the previous middleware.

Common Mistakes

[CRITICAL] Forgetting to call and return opts.next()

Wrong:

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

const t = initTRPC.create();

const logMiddleware = t.middleware(async (opts) => {
  console.log('request started');
  // forgot to call opts.next()
});

Correct:

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

const t = initTRPC.create();

const logMiddleware = t.middleware(async (opts) => {
  console.log('request started');
  const result = await opts.next();
  console.log('request ended');
  return result;
});

Middleware must call opts.next() and return its result; forgetting this silently drops the request with an INTERNAL_SERVER_ERROR because no middleware marker is returned.

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

[HIGH] Extending context with wrong type in opts.next()

Wrong:

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

const t = initTRPC.create();

const middleware = t.middleware(async (opts) => {
  return opts.next({ ctx: 'not-an-object' });
});

Correct:

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

const t = initTRPC.create();

async function getUser() {
  return { id: '1', name: 'Katt' };
}

const middleware = t.middleware(async (opts) => {
  return opts.next({ ctx: { user: await getUser() } });
});

Context extension in opts.next({ ctx }) must be an object; passing non-object values or overwriting required keys breaks downstream procedures.

Source: www/docs/server/middlewares.md

See Also

  • server-setup -- initTRPC, routers, procedures, context
  • validators -- input/output validation with Zod
  • error-handling -- TRPCError codes used in auth middleware
  • auth -- full auth patterns combining middleware + client headers

Files

1
5.4 KB

Agent reviews

0

No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.

More from trpc/trpc8

adapter-aws-lambda

Deploy tRPC on AWS Lambda with awsLambdaRequestHandler() from @trpc/server/adapters/aws-lambda for API Gateway v1 (REST, APIGatewayProxyEvent) and v2 (HTTP, APIGatewayProxyEventV2), and Lambda Function URLs. Enable response streaming with awsLambdaStreamingRequestHandler() wrapped in awslambda.strea

Scan passed 0
adapter-express

Mount tRPC as Express middleware with createExpressMiddleware() from @trpc/server/adapters/express. Access Express req/res in createContext via CreateExpressContextOptions. Mount at a path prefix like app.use('/trpc', ...). Avoid global express.json() conflicting with tRPC body parsing for FormData.

Scan passed 0
adapter-fastify

Mount tRPC as a Fastify plugin with fastifyTRPCPlugin from @trpc/server/adapters/fastify. Configure prefix, trpcOptions (router, createContext, onError). Enable WebSocket subscriptions with useWSS and @fastify/websocket. Set routerOptions.maxParamLength for batch requests. Requires Fastify v5+. Fast

Scan passed 0
adapter-fetch

Deploy tRPC on WinterCG-compliant edge runtimes with fetchRequestHandler() from @trpc/server/adapters/fetch. Supports Cloudflare Workers, Deno Deploy, Vercel Edge Runtime, Astro, Remix, SolidStart. FetchCreateContextFnOptions provides req (Request) and resHeaders (Headers) for context creation. The

Scan passed 0
adapter-standalone

Mount tRPC on Node.js built-in HTTP server with createHTTPServer() from @trpc/server/adapters/standalone, createHTTPHandler() for custom http.createServer, createHTTP2Handler() for HTTP/2 with TLS. Configure basePath to slice URL prefix, CORS via the cors npm package passed as middleware option. Cre

Scan passed 0
auth

Implement JWT/cookie authentication and authorization in tRPC using createContext for user extraction, t.middleware with opts.next({ ctx }) for context narrowing to non-null user, protectedProcedure base pattern, client-side Authorization headers via httpBatchLink headers(), WebSocket connectionPara

Scan passed 0
caching

Set HTTP cache headers on tRPC query responses via responseMeta callback for CDN and browser caching. Configure Cache-Control, s-maxage, stale-while-revalidate. Handle caching with batching and authenticated requests. Avoid caching mutations, errors, and authenticated responses.

Scan passed 0
client-setup

Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer on links (not client constructor). Infer types with inferRouterInputs and inferRouterOutputs. AbortController signal support. TRPCClientError typin

Scan passed 0

Related backend skillsscan passed