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>): http.Server<any, any>createHTTPServer } from '@trpc/server/adapters/standalone';
import type { type CreateHTTPContextOptions = {
req: http.IncomingMessage;
res: http.ServerResponse;
info: TRPCRequestInfo;
}
CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
export const const createContext: (opts: CreateHTTPContextOptions) => Promise<{
req: http.IncomingMessage;
res: http.ServerResponse;
}>
createContext = async (opts: CreateHTTPContextOptionsopts: type CreateHTTPContextOptions = {
req: http.IncomingMessage;
res: http.ServerResponse;
info: TRPCRequestInfo;
}
CreateHTTPContextOptions) => {
return {
req: http.IncomingMessagereq: opts: CreateHTTPContextOptionsopts.req: http.IncomingMessagereq,
res: http.ServerResponseres: opts: CreateHTTPContextOptionsopts.res: http.ServerResponseres,
};
};
type type Context = {
req: http.IncomingMessage;
res: http.ServerResponse;
}
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: http.IncomingMessage;
res: http.ServerResponse;
}>
createContext>>;
export const const t: TRPCRootObject<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object, RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>, {
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.context<{
req: http.IncomingMessage;
res: http.ServerResponse;
}>(): TRPCBuilder<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>
context<type Context = {
req: http.IncomingMessage;
res: http.ServerResponse;
}
Context>().TRPCBuilder<{ req: http.IncomingMessage; res: http.ServerResponse; }, object>.create<RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>>(opts?: RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object> | undefined): TRPCRootObject<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object, RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>, {
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
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(handler: TimerHandler, timeout?: number, ...arguments: any[]): numbersetTimeout(resolve: (value: unknown) => voidresolve, ms: numberms));
export const const appRouter: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter = const t: TRPCRootObject<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object, RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>, {
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<{ req: http.IncomingMessage; res: http.ServerResponse; }, object, RuntimeConfigOptions<{ req: http.IncomingMessage; res: http.ServerResponse; }, object>, { ctx: { req: http.IncomingMessage; res: http.ServerResponse; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>(_: {
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}) => BuiltRouter<...>
router({
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
public: const t: TRPCRootObject<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object, RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>, {
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<{ req: http.IncomingMessage; res: http.ServerResponse; }, object, RuntimeConfigOptions<{ req: http.IncomingMessage; res: http.ServerResponse; }, object>, { ctx: { req: http.IncomingMessage; res: http.ServerResponse; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.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: http.IncomingMessage;
res: http.ServerResponse;
};
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: http.IncomingMessage;
res: http.ServerResponse;
}, object, RuntimeConfigOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object>, {
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<{ req: http.IncomingMessage; res: http.ServerResponse; }, object, RuntimeConfigOptions<{ req: http.IncomingMessage; res: http.ServerResponse; }, object>, { ctx: { req: http.IncomingMessage; res: http.ServerResponse; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
procedure.ProcedureBuilder<{ req: http.IncomingMessage; res: http.ServerResponse; }, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
lastUpdated: string;
}>(resolver: ProcedureResolver<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, object, object, UnsetMarker, UnsetMarker, {
lastUpdated: string;
}>): QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>
query(async (opts: ProcedureResolverOptions<{
req: http.IncomingMessage;
res: http.ServerResponse;
}, 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: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
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: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
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: http.Server<any, any>server = createHTTPServer<BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>>(opts: CreateHTTPHandlerOptions<...>): http.Server<...>
createHTTPServer({
BaseHandlerOptions<BuiltRouter<{ ctx: { req: http.IncomingMessage; res: http.ServerResponse; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>, http.IncomingMessage>.router: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter,
createContext: NodeHTTPCreateContextFn<BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, http.IncomingMessage, http.ServerResponse>
createContext,
HTTPBaseHandlerOptions<BuiltRouter<{ ctx: { req: http.IncomingMessage; res: http.ServerResponse; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>, http.IncomingMessage>.responseMeta?: ResponseMetaFn<BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
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: http.IncomingMessage;
res: http.ServerResponse;
} | undefined;
paths: readonly string[] | undefined;
info: TRPCRequestInfo | undefined;
type: ProcedureType | "unknown";
errors: TRPCError[];
eagerGeneration: boolean;
}
opts) {
const { const paths: readonly string[] | undefinedpaths, const errors: TRPCError[]errors, const type: "query" | "mutation" | "subscription" | "unknown"type } = opts: {
data: TRPCResponse<unknown, DefaultErrorShape>[];
ctx?: {
req: http.IncomingMessage;
res: http.ServerResponse;
} | undefined;
paths: readonly string[] | undefined;
info: TRPCRequestInfo | undefined;
type: ProcedureType | "unknown";
errors: TRPCError[];
eagerGeneration: boolean;
}
opts;
// assuming you have all your public routes with the keyword `public` in them
const const allPublic: boolean | undefinedallPublic = const paths: readonly string[] | undefinedpaths && const paths: readonly string[]paths.ReadonlyArray<string>.every(predicate: (value: string, index: number, array: readonly string[]) => unknown, thisArg?: any): boolean (+1 overload)every((path: stringpath) => path: stringpath.String.includes(searchString: string, position?: number): booleanincludes('public'));
// checking that no procedures errored
const const allOk: booleanallOk = 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: http.Server<any, any>server.listen(3000);