Skip to content
tRPC

Partners

Response Caching

On this page

Since all tRPC queries are normal HTTP GET requests, you can use standard HTTP cache headers to cache responses. This can make responses snappy, give your database a rest, and help scale your API.

Using responseMeta to cache responses

Most tRPC adapters support a responseMeta callback that lets you set HTTP headers (including cache headers) based on the procedures being called.

This works with any hosting provider that supports standard HTTP cache headers (e.g. Vercel, Cloudflare, AWS CloudFront).

server.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
import { function createHTTPServer<TRouter extends AnyRouter>(opts: CreateHTTPHandlerOptions<TRouter>): Server<typeof IncomingMessage, typeof ServerResponse>createHTTPServer } from '@trpc/server/adapters/standalone';
import type { 
type CreateHTTPContextOptions = {
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
    info: TRPCRequestInfo;
}
CreateHTTPContextOptions
} from '@trpc/server/adapters/standalone';
export const
const createContext: (opts: CreateHTTPContextOptions) => Promise<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}>
createContext
= async (opts: CreateHTTPContextOptionsopts:
type CreateHTTPContextOptions = {
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
    info: TRPCRequestInfo;
}
CreateHTTPContextOptions
) => {
return { req: IncomingMessagereq: opts: CreateHTTPContextOptionsopts.req: IncomingMessagereq, res: ServerResponse<IncomingMessage>res: opts: CreateHTTPContextOptionsopts.res: ServerResponse<IncomingMessage>res, }; }; type
type Context = {
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}
Context
=
type Awaited<T> = T extends null | undefined ? T : T extends object & {
    then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T
Awaited
<type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof
const createContext: (opts: CreateHTTPContextOptions) => Promise<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}>
createContext
>>;
export const
const t: TRPCRootObject<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>, {
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.context<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}>(): TRPCBuilder<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>
context
<
type Context = {
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}
Context
>().
TRPCBuilder<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object>.create<RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>>(opts?: RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object> | undefined): TRPCRootObject<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>, {
    ...;
}>
create
();
const const waitFor: (ms: number) => Promise<unknown>waitFor = async (ms: numberms: number) => new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
Promise
((resolve: (value: unknown) => voidresolve) => function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)setTimeout(resolve: (value: unknown) => voidresolve, ms: numberms));
export const
const appRouter: BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>
appRouter
=
const t: TRPCRootObject<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>, {
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object, RuntimeConfigOptions<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object>, { ...; }>.router: RouterBuilder
<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>(_: {
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}) => BuiltRouter<...>
router
({
public: BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    slowQueryCached: QueryProcedure<{
        input: void;
        output: {
            lastUpdated: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
public
:
const t: TRPCRootObject<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>, {
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object, RuntimeConfigOptions<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object>, { ...; }>.router: RouterBuilder
<{
    slowQueryCached: QueryProcedure<{
        input: void;
        output: {
            lastUpdated: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    slowQueryCached: QueryProcedure<{
        input: void;
        output: {
            lastUpdated: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    slowQueryCached: QueryProcedure<{
        input: void;
        output: {
            lastUpdated: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
slowQueryCached: QueryProcedure<{
    input: void;
    output: {
        lastUpdated: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
slowQueryCached
:
const t: TRPCRootObject<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, RuntimeConfigOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object>, {
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object, RuntimeConfigOptions<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object>, { ...; }>.procedure: ProcedureBuilder<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
procedure
.
ProcedureBuilder<{ req: IncomingMessage; res: ServerResponse<IncomingMessage>; }, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
    lastUpdated: string;
}>(resolver: ProcedureResolver<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, object, UnsetMarker, UnsetMarker, {
    lastUpdated: string;
}>): QueryProcedure<{
    input: void;
    output: {
        lastUpdated: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
query
(async (
opts: ProcedureResolverOptions<{
    req: IncomingMessage;
    res: ServerResponse<IncomingMessage>;
}, object, object, UnsetMarker>
opts
) => {
await const waitFor: (ms: number) => Promise<unknown>waitFor(5000); // wait for 5s return { lastUpdated: stringlastUpdated: new
var Date: DateConstructor
new () => Date (+3 overloads)
Date
().Date.toJSON(key?: any): stringtoJSON(),
}; }), }), }); // Exporting `type AppRouter` only exposes types that can be used for inference // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export export type
type AppRouter = Router<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
= typeof
const appRouter: BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>
appRouter
;
// export API handler const const server: Server<typeof IncomingMessage, typeof ServerResponse>server =
createHTTPServer<BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>(opts: CreateHTTPHandlerOptions<...>): Server<typeof IncomingMessage, typeof ServerResponse>
createHTTPServer
({
BaseHandlerOptions<BuiltRouter<{ ...; }, DecorateCreateRouterOptions<...>>, IncomingMessage>.router: BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>
router
:
const appRouter: BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>
appRouter
,
createContext: NodeHTTPCreateContextFn<BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>, IncomingMessage, ServerResponse<...>>
createContext
,
HTTPBaseHandlerOptions<BuiltRouter<{ ...; }, DecorateCreateRouterOptions<...>>, IncomingMessage>.responseMeta?: ResponseMetaFn<BuiltRouter<{
    ctx: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            req: IncomingMessage;
            res: ServerResponse<IncomingMessage>;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        slowQueryCached: QueryProcedure<{
            input: void;
            output: {
                lastUpdated: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>> | undefined
responseMeta
(
opts: {
    data: TRPCResponse<unknown, DefaultErrorShape>[];
    ctx?: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    } | undefined;
    paths: readonly string[] | undefined;
    info: TRPCRequestInfo | undefined;
    type: ProcedureType | "unknown";
    errors: TRPCError[];
    eagerGeneration: boolean;
}
opts
) {
const { const info: TRPCRequestInfo | undefinedinfo, const errors: TRPCError[]errors, const type: "query" | "mutation" | "subscription" | "unknown"type, const eagerGeneration: booleaneagerGeneration } =
opts: {
    data: TRPCResponse<unknown, DefaultErrorShape>[];
    ctx?: {
        req: IncomingMessage;
        res: ServerResponse<IncomingMessage>;
    } | undefined;
    paths: readonly string[] | undefined;
    info: TRPCRequestInfo | undefined;
    type: ProcedureType | "unknown";
    errors: TRPCError[];
    eagerGeneration: boolean;
}
opts
;
// assuming all your public routes are in the `public` router const const allPublic: boolean | undefinedallPublic = const info: TRPCRequestInfo | undefinedinfo?.TRPCRequestInfo.calls: TRPCRequestInfoProcedureCall[]calls.Array<TRPCRequestInfoProcedureCall>.every<S>(predicate: (value: TRPCRequestInfoProcedureCall, index: number, array: TRPCRequestInfoProcedureCall[]) => value is S, thisArg?: any): this is S[] (+1 overload)every((call: TRPCRequestInfoProcedureCallcall) => call: TRPCRequestInfoProcedureCallcall.TRPCRequestInfoProcedureCall.path: stringpath.String.startsWith(searchString: string, position?: number): booleanstartsWith('public.'), ); // checking that no procedures errored // (when streaming, headers are sent before any results are known) const const allOk: booleanallOk = !const eagerGeneration: booleaneagerGeneration && const errors: TRPCError[]errors.Array<T>.length: numberlength === 0; // checking we're doing a query request const const isQuery: booleanisQuery = const type: "query" | "mutation" | "subscription" | "unknown"type === 'query'; if (const allPublic: boolean | undefinedallPublic && const allOk: booleanallOk && const isQuery: booleanisQuery) { // cache request for 1 day + revalidate once every second const const ONE_DAY_IN_SECONDS: numberONE_DAY_IN_SECONDS = 60 * 60 * 24; return { ResponseMeta.headers?: Headers | HTTPHeaders | undefinedheaders: new var Headers: new (init?: HeadersInit) => HeadersHeaders([ [ 'cache-control', `s-maxage=1, stale-while-revalidate=${const ONE_DAY_IN_SECONDS: numberONE_DAY_IN_SECONDS}`, ], ]), }; } return {}; }, }); const server: Server<typeof IncomingMessage, typeof ServerResponse>server.Server.listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): Server<typeof IncomingMessage, typeof ServerResponse> (+8 overloads)listen(3000);

responseMeta runs once per HTTP response, so with batching it decides the headers for every procedure in the batch: only cache if all of them are public. It also gets the request's ctx, e.g. to skip caching for signed-in users. For streamed responses, like those of httpBatchStreamLink, the headers are sent before any procedure has finished, so responseMeta runs with eagerGeneration: true and an empty errors array.

Next.js App Router

With the App Router, tRPC runs in a route handler with the fetch adapter. Next.js sends the route handler's Response as it is, so set Cache-Control in the responseMeta of fetchRequestHandler. Route segment config like export const dynamic = 'force-dynamic' or export const revalidate = 60 doesn't change the header.

app/api/trpc/[trpc]/route.ts
import { function fetchRequestHandler<TRouter extends AnyRouter>(opts: FetchHandlerRequestOptions<TRouter>): Promise<Response>fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { 
const createTRPCContext: (opts: {
    headers: Headers;
}) => Promise<{
    userId: string;
}>
createTRPCContext
} from './trpc/init';
import {
const appRouter: BuiltRouter<{
    ctx: {
        userId: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            userId: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    me: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
} from './trpc/routers/_app';
const const handler: (req: Request) => Promise<Response>handler = (req: Requestreq: Request) =>
fetchRequestHandler<BuiltRouter<{
    ctx: {
        userId: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            userId: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    me: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>(opts: FetchHandlerRequestOptions<...>): Promise<Response>
fetchRequestHandler
({
endpoint: stringendpoint: '/api/trpc', req: Requestreq,
BaseHandlerOptions<BuiltRouter<{ ctx: { userId: string; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ public: BuiltRouter<{ ctx: { userId: string; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; me: QueryProcedure<...>; }>>, Request>.router: BuiltRouter<{
    ctx: {
        userId: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            userId: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    me: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
:
const appRouter: BuiltRouter<{
    ctx: {
        userId: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            userId: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    me: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
,
createContext: FetchCreateContextFn<BuiltRouter<{
    ctx: {
        userId: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            userId: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    me: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>
createContext
: () =>
function createTRPCContext(opts: {
    headers: Headers;
}): Promise<{
    userId: string;
}>
createTRPCContext
({ headers: Headersheaders: req: Requestreq.Request.headers: Headersheaders }),
HTTPBaseHandlerOptions<BuiltRouter<{ ctx: { userId: string; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ public: BuiltRouter<{ ctx: { userId: string; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; me: QueryProcedure<...>; }>>, Request>.responseMeta?: ResponseMetaFn<BuiltRouter<{
    ctx: {
        userId: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: {
            userId: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
    me: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>> | undefined
responseMeta
(
opts: {
    data: TRPCResponse<unknown, DefaultErrorShape>[];
    ctx?: {
        userId: string;
    } | undefined;
    paths: readonly string[] | undefined;
    info: TRPCRequestInfo | undefined;
    type: ProcedureType | "unknown";
    errors: TRPCError[];
    eagerGeneration: boolean;
}
opts
) {
const { const info: TRPCRequestInfo | undefinedinfo, const errors: TRPCError[]errors, const type: "query" | "mutation" | "subscription" | "unknown"type, const eagerGeneration: booleaneagerGeneration } =
opts: {
    data: TRPCResponse<unknown, DefaultErrorShape>[];
    ctx?: {
        userId: string;
    } | undefined;
    paths: readonly string[] | undefined;
    info: TRPCRequestInfo | undefined;
    type: ProcedureType | "unknown";
    errors: TRPCError[];
    eagerGeneration: boolean;
}
opts
;
// assuming all your public routes are in the `public` router const const allPublic: boolean | undefinedallPublic = const info: TRPCRequestInfo | undefinedinfo?.TRPCRequestInfo.calls: TRPCRequestInfoProcedureCall[]calls.Array<TRPCRequestInfoProcedureCall>.every<S>(predicate: (value: TRPCRequestInfoProcedureCall, index: number, array: TRPCRequestInfoProcedureCall[]) => value is S, thisArg?: any): this is S[] (+1 overload)every((call: TRPCRequestInfoProcedureCallcall) => call: TRPCRequestInfoProcedureCallcall.TRPCRequestInfoProcedureCall.path: stringpath.String.startsWith(searchString: string, position?: number): booleanstartsWith('public.'), ); const const allOk: booleanallOk = !const eagerGeneration: booleaneagerGeneration && const errors: TRPCError[]errors.Array<T>.length: numberlength === 0; const const isQuery: booleanisQuery = const type: "query" | "mutation" | "subscription" | "unknown"type === 'query'; if (const allPublic: boolean | undefinedallPublic && const allOk: booleanallOk && const isQuery: booleanisQuery) { return { ResponseMeta.headers?: Headers | HTTPHeaders | undefinedheaders: new var Headers: new (init?: HeadersInit) => HeadersHeaders([ ['cache-control', 's-maxage=60, stale-while-revalidate=86400'], ]), }; } return {}; }, }); export { const handler: (req: Request) => Promise<Response>handler as
const GET: (req: Request) => Promise<Response>
export GET
GET
, const handler: (req: Request) => Promise<Response>handler as
const POST: (req: Request) => Promise<Response>
export POST
POST
};

Batching

responseMeta runs once per batch, not once per procedure. With httpBatchLink, a public query made at the same time as a private one shares its request, so the response isn't cached. And a CDN caches each batch by its URL, which holds every procedure and input in the batch, so the same public query rarely hits the cache.

Send cacheable public queries through httpLink with a splitLink, so each one is a GET request of its own, and keep batching the rest. getUrl() is the one from the App Router setup:

trpc/client.tsx
import {
  function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
  function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
  function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpLink,
  
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
    false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink
,
} from '@trpc/client'; import type {
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>> & DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>
AppRouter
} from './routers/_app';
const
const trpcClient: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>
trpcClient
=
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>): TRPCClient<...>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>> & DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>[]
links
: [
splitLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<...> | TRPCLink<...>[];
    false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink
({
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'query' && op: Operationop.path: stringpath.String.startsWith(searchString: string, position?: number): booleanstartsWith('public.'),
true: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>> | TRPCLink<...>[]
true
:
httpLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>(opts: HTTPLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<...>
httpLink
({ url: string | URLurl: function getUrl(): stringgetUrl() }),
false: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>> | TRPCLink<...>[]
false
:
httpBatchLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    public: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        posts: QueryProcedure<{
            input: void;
            output: {
                title: string;
            }[];
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
    }>>;
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<...>
httpBatchLink
({ url: string | URLurl: function getUrl(): stringgetUrl() }),
}), ], });