Server-Side Rendering
Prefetch a page's tRPC queries in getServerSideProps with createTRPCOptionsProxy, build the context from the request, and set the status and cache headers
On this page
To render a page's data on the server, prefetch its queries in getServerSideProps. Next.js then renders the page with the data in the cache, so it's in the first HTML, and the client doesn't fetch it again.
This page builds on the Pages Router setup.
Prefetch in getServerSideProps
In getServerSideProps, make a query client and a tRPC proxy that calls your router directly, with no HTTP request. Prefetch the queries the page needs, and return the cache as trpcState. _app's HydrationBoundary puts it into the client's cache.
import { function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate, function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<TData, TError> (+2 overloads)useQuery } from '@tanstack/react-query';
import { function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type {
type GetServerSidePropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
req: IncomingMessage & {
cookies: NextApiRequestCookies;
};
res: ServerResponse;
params?: Params;
query: ParsedUrlQuery;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
}
GetServerSidePropsContext,
type InferGetServerSidePropsType<T extends (args: any) => any> = Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"] extends null | undefined ? Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"] : Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"] extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"]
InferGetServerSidePropsType,
} from 'next';
import { function createContext(opts: {
headers: IncomingHttpHeaders;
}): Promise<{
user: {
id: string;
} | null;
}>
createContext } from '../../server/context';
import { const appRouter: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter } from '../../server/routers/_app';
import { function makeQueryClient(): QueryClientmakeQueryClient } from '../../utils/query-client';
import { const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
useTRPC } from '../../utils/trpc';
export async function function getServerSideProps(context: GetServerSidePropsContext<{
id: string;
}>): Promise<{
props: {
trpcState: DehydratedState;
id: string;
};
}>
getServerSideProps(
context: GetServerSidePropsContext<{
id: string;
}>
context: type GetServerSidePropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
req: IncomingMessage & {
cookies: NextApiRequestCookies;
};
res: ServerResponse;
params?: Params;
query: ParsedUrlQuery;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
}
GetServerSidePropsContext<{ id: stringid: string }>,
) {
const const id: stringid = context: GetServerSidePropsContext<{
id: string;
}>
context.params?: {
id: string;
} | undefined
params?.id: string | undefinedid as string;
const const queryClient: QueryClientqueryClient = function makeQueryClient(): QueryClientmakeQueryClient();
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc = createTRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>(opts: TRPCOptionsProxyOptions<...>): TRPCOptionsProxy<...>
createTRPCOptionsProxy({
TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: { user: { id: string; } | null; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: { user: { id: string; } | null; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; }>>>.router: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter,
TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: { user: { id: string; } | null; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: { user: { id: string; } | null; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; }>>>.ctx: {
user: {
id: string;
} | null;
} | (() => MaybePromise<{
user: {
id: string;
} | null;
}>)
ctx: await function createContext(opts: {
headers: IncomingHttpHeaders;
}): Promise<{
user: {
id: string;
} | null;
}>
createContext({ headers: IncomingHttpHeadersheaders: context: GetServerSidePropsContext<{
id: string;
}>
context.req: IncomingMessage & {
cookies: NextApiRequestCookies;
}
req.IncomingMessage.headers: IncomingHttpHeadersheaders }),
queryClient: QueryClient | (() => QueryClient)queryClient,
});
await const queryClient: QueryClientqueryClient.QueryClient.prefetchQuery<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
text: string;
}, TRPCQueryKeyWithoutPrefix>(options: FetchQueryOptions<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
text: string;
}, TRPCQueryKeyWithoutPrefix, never>): Promise<void>
prefetchQuery(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId.DecorateQueryProcedure<{ input: { id: string; }; output: { id: string; title: string; text: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<{
id: string;
title: string;
text: string;
}, {
id: string;
title: string;
text: string;
}>(input: {
id: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<{
id: string;
title: string;
text: string;
}, {
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<{
id: string;
title: string;
text: string;
}, {
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> (+2 overloads)
queryOptions({ id: stringid }));
return {
props: {
trpcState: DehydratedState;
id: string;
}
props: {
trpcState: DehydratedStatetrpcState: function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate(const queryClient: QueryClientqueryClient),
id: stringid,
},
};
}
export default function function PostPage(props: InferGetServerSidePropsType<typeof getServerSideProps>): JSX.ElementPostPage(
props: {
trpcState: DehydratedState;
id: string;
}
props: type InferGetServerSidePropsType<T extends (args: any) => any> = Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"] extends null | undefined ? Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"] : Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"] extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"]
InferGetServerSidePropsType<typeof function getServerSideProps(context: GetServerSidePropsContext<{
id: string;
}>): Promise<{
props: {
trpcState: DehydratedState;
id: string;
};
}>
getServerSideProps>,
) {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
useTRPC();
const const postQuery: UseQueryResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
postQuery = useQuery<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
text: string;
}, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
text: string;
}, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId.DecorateQueryProcedure<{ input: { id: string; }; output: { id: string; title: string; text: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<{
id: string;
title: string;
text: string;
}, {
id: string;
title: string;
text: string;
}>(input: {
id: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<{
id: string;
title: string;
text: string;
}, {
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<{
id: string;
title: string;
text: string;
}, {
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> (+2 overloads)
queryOptions({ id: stringid: props: {
trpcState: DehydratedState;
id: string;
}
props.id: stringid }));
switch (const postQuery: UseQueryResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
postQuery.status: "pending" | "error" | "success"status) {
case 'pending':
return <JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading...</JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'error':
return <JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Error: {const postQuery: QueryObserverRefetchErrorResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>> | QueryObserverLoadingErrorResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
postQuery.error: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>
error.TRPCClientErrorBase<DefaultErrorShape>.message: stringmessage}</JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'success':
return (
<JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article>
<JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const postQuery: QueryObserverSuccessResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>> | QueryObserverPlaceholderResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
postQuery.data: {
id: string;
title: string;
text: string;
}
data.title: stringtitle}</JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
<JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const postQuery: QueryObserverSuccessResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>> | QueryObserverPlaceholderResult<{
id: string;
title: string;
text: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
postQuery.data: {
id: string;
title: string;
text: string;
}
data.text: stringtext}</JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
</JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article>
);
}
}The page reads the query as it would without prefetching, so status is 'success' on the first render. Use the same input in both places: it's part of the query key.
prefetchQuery never throws, and the cache only sends queries that succeeded, so a failed query fetches again in the browser. To act on an error on the server, use fetchQuery, which returns the data or throws. See Set the status and cache headers.
Build the context from the request
The proxy calls your procedures in the same process, so there's no URL to set and no headers to forward. Build the context from context.req instead, as the API handler does. Sharing one createContext that only needs the headers keeps the two the same:
import type { IncomingHttpHeaders } from 'node:http';
import { function getUserFromCookie(cookie: string | undefined): Promise<{
id: string;
} | null>
getUserFromCookie } from './auth';
export async function function createContext(opts: {
headers: IncomingHttpHeaders;
}): Promise<{
user: {
id: string;
} | null;
}>
createContext(opts: {
headers: IncomingHttpHeaders;
}
opts: { headers: IncomingHttpHeadersheaders: IncomingHttpHeaders }) {
// verify the session here: headers come from the client, so don't
// trust a user ID that's simply sent in one
const const user: {
id: string;
} | null
user = await function getUserFromCookie(cookie: string | undefined): Promise<{
id: string;
} | null>
getUserFromCookie(opts: {
headers: IncomingHttpHeaders;
}
opts.headers: IncomingHttpHeadersheaders.IncomingHttpHeaders.cookie?: string | undefinedcookie);
return { user: {
id: string;
} | null
user };
}import * as import trpcNexttrpcNext from '@trpc/server/adapters/next';
import { function createContext(opts: {
headers: IncomingHttpHeaders;
}): Promise<{
user: {
id: string;
} | null;
}>
createContext } from '../../../server/context';
import { const appRouter: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter } from '../../../server/routers/_app';
export default import trpcNexttrpcNext.function createNextApiHandler<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: NodeHTTPHandlerOptions<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse>): trpcNext.NextApiHandler
createNextApiHandler({
BaseHandlerOptions<BuiltRouter<{ ctx: { user: { id: string; } | null; }; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, NextApiRequest>.router: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
router: const appRouter: BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter,
createContext: NodeHTTPCreateContextFn<BuiltRouter<{
ctx: {
user: {
id: string;
} | null;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse>
createContext: (opts: NodeHTTPCreateContextFnOptions<trpcNext.NextApiRequest, trpcNext.NextApiResponse>opts) => function createContext(opts: {
headers: IncomingHttpHeaders;
}): Promise<{
user: {
id: string;
} | null;
}>
createContext({ headers: IncomingHttpHeadersheaders: opts: NodeHTTPCreateContextFnOptions<trpcNext.NextApiRequest, trpcNext.NextApiResponse>opts.req: trpcNext.NextApiRequestreq.IncomingMessage.headers: IncomingHttpHeadersheaders }),
});If your API runs on another server
Pass a tRPC client to createTRPCOptionsProxy instead of the router. The request now goes over HTTP, so it needs the API's full URL, and you forward the headers the API needs, such as the cookie:
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 } from '@trpc/client';
import { function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { type GetServerSidePropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
req: IncomingMessage & {
cookies: NextApiRequestCookies;
};
res: ServerResponse;
params?: Params;
query: ParsedUrlQuery;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
}
GetServerSidePropsContext } from 'next';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from '../../server/routers/_app';
import { function makeQueryClient(): QueryClientmakeQueryClient } from '../../utils/query-client';
export async function function getServerSideProps(context: GetServerSidePropsContext): Promise<void>getServerSideProps(context: GetServerSidePropsContextcontext: type GetServerSidePropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
req: IncomingMessage & {
cookies: NextApiRequestCookies;
};
res: ServerResponse;
params?: Params;
query: ParsedUrlQuery;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
}
GetServerSidePropsContext) {
const const queryClient: QueryClientqueryClient = function makeQueryClient(): QueryClientmakeQueryClient();
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
trpc = createTRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>, {
keyPrefix: false;
}>
createTRPCOptionsProxy({
TRPCOptionsProxyOptionsExternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>.client: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCUntypedClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
client: createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: 'https://api.example.com/trpc',
headers?: HTTPHeaders | ((opts: {
opList: NonEmptyArray<Operation>;
}) => HTTPHeaders | Promise<HTTPHeaders>) | undefined
headers: () => ({ cookie: string | undefinedcookie: context: GetServerSidePropsContextcontext.req: IncomingMessage & {
cookies: NextApiRequestCookies;
}
req.IncomingMessage.headers: IncomingHttpHeadersheaders.IncomingHttpHeaders.cookie?: string | undefinedcookie }),
}),
],
}),
queryClient: QueryClient | (() => QueryClient)queryClient,
});
// prefetch and dehydrate as above
}Set the status and cache headers
getServerSideProps gets the response as context.res, so you set headers on it directly. To send a 404 when a post doesn't exist, use fetchQuery, catch the TRPCError, and return notFound:
import { function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate } from '@tanstack/react-query';
import { class TRPCErrorTRPCError } from '@trpc/server';
import { function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { type GetServerSidePropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
req: IncomingMessage & {
cookies: NextApiRequestCookies;
};
res: ServerResponse;
params?: Params;
query: ParsedUrlQuery;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
}
GetServerSidePropsContext } from 'next';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter } from '../../server/routers/_app';
import { function makeQueryClient(): QueryClientmakeQueryClient } from '../../utils/query-client';
export async function function getServerSideProps(context: GetServerSidePropsContext<{
id: string;
}>): Promise<{
notFound: boolean;
props?: undefined;
} | {
props: {
trpcState: DehydratedState;
id: string;
};
notFound?: undefined;
}>
getServerSideProps(
context: GetServerSidePropsContext<{
id: string;
}>
context: type GetServerSidePropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
req: IncomingMessage & {
cookies: NextApiRequestCookies;
};
res: ServerResponse;
params?: Params;
query: ParsedUrlQuery;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
}
GetServerSidePropsContext<{ id: stringid: string }>,
) {
const const id: stringid = context: GetServerSidePropsContext<{
id: string;
}>
context.params?: {
id: string;
} | undefined
params?.id: string | undefinedid as string;
const const queryClient: QueryClientqueryClient = function makeQueryClient(): QueryClientmakeQueryClient();
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc = createTRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>(opts: TRPCOptionsProxyOptions<...>): TRPCOptionsProxy<...>
createTRPCOptionsProxy({
TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; }>>>.router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter,
TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; }>>>.ctx: object | (() => MaybePromise<object>)ctx: {},
queryClient: QueryClient | (() => QueryClient)queryClient,
});
try {
await const queryClient: QueryClientqueryClient.QueryClient.fetchQuery<{
id: string;
title: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
}, TRPCQueryKeyWithoutPrefix, never>(options: FetchQueryOptions<{
id: string;
title: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
}, TRPCQueryKeyWithoutPrefix, never>): Promise<{
id: string;
title: string;
}>
fetchQuery(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId.DecorateQueryProcedure<{ input: { id: string; }; output: { id: string; title: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<{
id: string;
title: string;
}, {
id: string;
title: string;
}>(input: {
id: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<{
id: string;
title: string;
}, {
id: string;
title: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<{
id: string;
title: string;
}, {
id: string;
title: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> (+2 overloads)
queryOptions({ id: stringid }));
} catch (function (local var) cause: unknowncause) {
if (function (local var) cause: unknowncause instanceof class TRPCErrorTRPCError && function (local var) cause: TRPCErrorcause.TRPCError.code: "BAD_GATEWAY" | "BAD_REQUEST" | "CLIENT_CLOSED_REQUEST" | "CONFLICT" | "FORBIDDEN" | "GATEWAY_TIMEOUT" | "INTERNAL_SERVER_ERROR" | "METHOD_NOT_SUPPORTED" | "NOT_FOUND" | "NOT_IMPLEMENTED" | "PARSE_ERROR" | "PAYLOAD_TOO_LARGE" | "PAYMENT_REQUIRED" | "PRECONDITION_FAILED" | "PRECONDITION_REQUIRED" | "SERVICE_UNAVAILABLE" | "TIMEOUT" | "TOO_MANY_REQUESTS" | "UNAUTHORIZED" | "UNPROCESSABLE_CONTENT" | "UNSUPPORTED_MEDIA_TYPE"code === 'NOT_FOUND') {
return { notFound: booleannotFound: true };
}
throw function (local var) cause: unknowncause;
}
// cache the page for 1 day, and revalidate it once a second at most
const const ONE_DAY_IN_SECONDS: numberONE_DAY_IN_SECONDS = 60 * 60 * 24;
context: GetServerSidePropsContext<{
id: string;
}>
context.res: ServerResponse<IncomingMessage>res.OutgoingMessage<IncomingMessage>.setHeader(name: string, value: number | string | readonly string[]): ServerResponse<IncomingMessage>setHeader(
'cache-control',
`s-maxage=1, stale-while-revalidate=${const ONE_DAY_IN_SECONDS: numberONE_DAY_IN_SECONDS}`,
);
return {
props: {
trpcState: DehydratedState;
id: string;
}
props: {
trpcState: DehydratedStatetrpcState: function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate(const queryClient: QueryClientqueryClient),
id: stringid,
},
};
}Only cache pages that look the same for everyone. If the page depends on the user's cookie, leave out the cache-control header. See also the general Response Caching docs.
API Response Caching with Next.js Adapter
You can also use responseMeta on the Next.js API handler to cache API responses directly:
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
import * as import trpcNexttrpcNext from '@trpc/server/adapters/next';
export const const createContext: ({ req, res, }: trpcNext.CreateNextContextOptions) => Promise<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}>
createContext = async ({
req: trpcNext.NextApiRequestreq,
res: trpcNext.NextApiResponseres,
}: import trpcNexttrpcNext.type CreateNextContextOptions = {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
info: TRPCRequestInfo;
}
CreateNextContextOptions) => {
return {
req: trpcNext.NextApiRequestreq,
res: trpcNext.NextApiResponseres,
};
};
type type Context = {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}
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: ({ req, res, }: trpcNext.CreateNextContextOptions) => Promise<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}>
createContext>>;
export const const t: TRPCRootObject<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>, {
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.context<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}>(): TRPCBuilder<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>
context<type Context = {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}
Context>().TRPCBuilder<{ req: NextApiRequest; res: NextApiResponse; }, object>.create<RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>>(opts?: RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object> | undefined): TRPCRootObject<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>, {
...;
}>
create();
export const const appRouter: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter = const t: TRPCRootObject<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>, {
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<{ req: NextApiRequest; res: NextApiResponse; }, object, RuntimeConfigOptions<{ req: NextApiRequest; res: NextApiResponse; }, object>, { ...; }>.router: RouterBuilder
<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>(_: {
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}) => BuiltRouter<...>
router({
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
public: const t: TRPCRootObject<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>, {
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<{ req: NextApiRequest; res: NextApiResponse; }, object, RuntimeConfigOptions<{ req: NextApiRequest; res: NextApiResponse; }, 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: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
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: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, RuntimeConfigOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object>, {
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<{ req: NextApiRequest; res: NextApiResponse; }, object, RuntimeConfigOptions<{ req: NextApiRequest; res: NextApiResponse; }, object>, { ...; }>.procedure: ProcedureBuilder<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
procedure.ProcedureBuilder<{ req: NextApiRequest; res: NextApiResponse; }, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
lastUpdated: string;
}>(resolver: ProcedureResolver<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, object, UnsetMarker, UnsetMarker, {
lastUpdated: string;
}>): QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>
query(async (opts: ProcedureResolverOptions<{
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
}, object, object, UnsetMarker>
opts) => {
await 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, 5000));
return {
lastUpdated: stringlastUpdated: new var Date: DateConstructor
new () => Date (+3 overloads)
Date().Date.toJSON(key?: any): stringtoJSON(),
};
}),
}),
});
export type type AppRouter = Router<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
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: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter;
export default import trpcNexttrpcNext.function createNextApiHandler<BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>>(opts: NodeHTTPHandlerOptions<...>): trpcNext.NextApiHandler
createNextApiHandler({
BaseHandlerOptions<BuiltRouter<{ ...; }, DecorateCreateRouterOptions<...>>, NextApiRequest>.router: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter,
createContext: NodeHTTPCreateContextFn<BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
slowQueryCached: QueryProcedure<{
input: void;
output: {
lastUpdated: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse>
createContext,
HTTPBaseHandlerOptions<BuiltRouter<{ ...; }, DecorateCreateRouterOptions<...>>, NextApiRequest>.responseMeta?: ResponseMetaFn<BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
public: BuiltRouter<{
ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
};
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: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
} | undefined;
paths: readonly string[] | undefined;
info: TRPCRequestInfo | undefined;
type: ProcedureType | "unknown";
errors: TRPCError[];
eagerGeneration: boolean;
}
opts) {
const { const ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
} | undefined
ctx, const paths: readonly string[] | undefinedpaths, const errors: TRPCError[]errors, const type: "query" | "mutation" | "subscription" | "unknown"type } = opts: {
data: TRPCResponse<unknown, DefaultErrorShape>[];
ctx?: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
} | 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 ctx: {
req: trpcNext.NextApiRequest;
res: trpcNext.NextApiResponse;
} | undefined
ctx?.res: trpcNext.NextApiResponse | undefinedres && 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 {};
},
});FAQ
Q: Why does the client still fetch data the server prefetched?
TanStack React Query refetches stale data when a component mounts, when the window regains focus, and when the network reconnects. The query client from the setup keeps data fresh for 30 seconds with staleTime, so the first render uses what the server sent. Raise staleTime to fetch less, or see SSG to turn refetching off.
Q: Can I use useSuspenseQuery?
Yes, for queries the page prefetched. A query that isn't in the cache suspends during the server render too, and the server can't reach your API's relative URL. Prefetch it in getServerSideProps, or use useQuery, which only fetches in the browser.