tRPC-Nuxt
repository·main·Indexed 21 days ago
https://github.com/wobsoriano/trpc-nuxtEnd-to-end typesafe API communication between a tRPC server and Nuxt applications. It allows the client to import type declarations without including server-side code in the client bundle. Features include support for authorization via context and middleware, optimistic UI updates using useNuxtData, HTTP response caching with responseMeta, and server-side procedure execution using createCaller().
What's inside trpc-nuxt
- tRPC-Nuxt provides end-to-end typesafe APIs for Nuxt applications using tRPC.io. A key feature of this integration is that the client does not import any actual server-side code; it only imports the type declarations, ensuring type safety without bloating the client bundle with server logic.
Understand the documentation project structure
mainThe documentation site follows the Astro + Starlight directory structure:
src/content/docs/: Contains.mdor.mdxfiles. Each file is exposed as a route based on its filename.src/assets/: Place images here to embed them in Markdown using relative links.public/: Place static assets like favicons here.astro.config.mjs: Astro configuration file.content.config.ts: Starlight content configuration.
. ├── public/ ├── src/ │ ├── assets/ │ ├── content/ │ │ ├── docs/ │ └── content.config.ts ├── astro.config.mjs ├── package.json └── tsconfig.jsonUse Nitro's Cache API for server-side tRPC calls
mainWhen performing server-side calls using a tRPC caller, you can leverage Nitro's
defineCachedEventHandlerto cache the results of those calls. This is useful for optimizing performance when calling your tRPC router from within Nuxt server routes.By wrapping your handler in
defineCachedEventHandler, you can specify caching strategies likeswr(stale-while-revalidate) andmaxAge.import { appRouter } from '~/server/trpc/routers'; const caller = appRouter.createCaller({}); export default defineCachedEventHandler( async (event) => { const { name } = getQuery(event); const greeting = await caller.greeting({ name }); return { greeting, }; }, { swr: true, maxAge: 10, }, );Configure HTTP response caching with responseMeta
mainTo enable edge caching (e.g., via Vercel's Edge Network), use the
responseMetaoption withincreateTRPCNuxtHandler. This function allows you to return custom headers, such ascache-control, which dictate how the server response should be cached by downstream proxies and CDNs.Ensure your response satisfies standard caching criteria as described in the tRPC.io documentation.
// server/api/trpc/[trpc].ts import { createTRPCNuxtHandler } from 'trpc-nuxt/server'; import { appRouter } from '~/server/trpc/routers'; export default createTRPCNuxtHandler({ router: appRouter, /** * @link https://trpc.io/docs/caching#api-response-caching */ responseMeta(opts) { // cache request for 1 day + revalidate once every second const ONE_DAY_IN_SECONDS = 60 * 60 * 24; return { headers: { 'cache-control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`, }, }; }, });Authorize using middleware
mainFor reusable authorization logic, use tRPC middleware. A middleware can intercept a request, check a condition in the context (e.g.,
ctx.user.isAdmin), and either throw aTRPCErrorwith code'UNAUTHORIZED'or callnext()to proceed. You can then create a specialized procedure type (e.g.,protectedProcedure) by calling.use(middleware)on a standard procedure, allowing you to easily protect multiple routes with the same logic.import { initTRPC, TRPCError } from '@trpc/server'; // ... context function const t = initTRPC.context<Context>().create(); const isAuthed = t.middleware(({ next, ctx }) => { if (!ctx.user?.isAdmin) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { user: ctx.user, }, }); }); // you can reuse this for any procedure export const protectedProcedure = t.procedure.use(isAuthed); t.router({ // this is accessible for everyone hello: t.procedure .input(z.string().nullish()) .query(({ input, ctx }) => `hello ${input ?? ctx.user?.name ?? 'world'}`), admin: t.router({ // this is accessible only to admins secret: protectedProcedure.query(({ ctx }) => { return { secret: 'sauce', }; }), }), });Authorize using a resolver
mainYou can perform authorization checks directly inside a tRPC resolver by inspecting the
ctxobject. If the user is not authorized, throw aTRPCErrorwith the code'UNAUTHORIZED'. This approach is useful for one-off checks or when authorization logic is highly specific to a single endpoint.import { initTRPC, TRPCError } from '@trpc/server'; // ... context function const t = initTRPC.context<Context>().create(); const appRouter = t.router({ // open for anyone hello: t.procedure .input(z.string().nullish()) .query(({ input, ctx }) => `hello ${input ?? ctx.user?.name ?? 'world'}`), // checked in resolver secret: t.procedure.query(({ ctx }) => { if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return { secret: 'sauce', }; }), });Set up the tRPC-Nuxt playground
mainTo set up the development playground environment, first ensure all project dependencies are installed using
pnpm.pnpm installCreate a type-safe tRPC client plugin
mainTo use tRPC in your Vue components with full type-safety, create a Nuxt plugin. Use
createTRPCNuxtClientand pass yourAppRoutertype. You must provide a link, such ashttpBatchLink, pointing to your tRPC endpoint.// plugins/trpc.ts import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'; import type { AppRouter } from '~/server/trpc/routers'; export default defineNuxtPlugin(() => { const trpc = createTRPCNuxtClient<AppRouter>({ links: [httpBatchLink({ url: '/api/trpc' })], }); return { provide: { trpc, }, }; });Perform optimistic updates with Mutation & Revalidation
mainYou can use Nuxt's
useNuxtDataalongsidetrpc-nuxtto perform optimistic UI updates. This pattern involves:- Generating a query key using
getQueryKeyfromtrpc-nuxt/client. - Accessing the cached query data via
useNuxtDatausing that key. - Manually updating the local
useNuxtDatastate before the mutation completes. - Using
refreshNuxtData(queryKey)to revalidate the data from the server upon a successful mutation. - Rolling back the local state if the mutation fails.
This ensures the UI feels instantaneous while maintaining data integrity through background revalidation.
<script setup lang="ts"> import type { inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from '~/server/trpc/routers'; import { getQueryKey } from 'trpc-nuxt/client'; const { $trpc } = useNuxtApp(); const previousTodos = ref([]); // 1. Generate the key for the specific query const queryKey = getQueryKey($trpc.getTodos, undefined); type RouterOutput = inferRouterOutputs<AppRouter>; // 2. Access the cached value of useQuery const { data: todos } = useNuxtData<RouterOutput['getTodos']>(queryKey); async function addTodo(payload) { // Store previous state for rollback previousTodos.value = [...todos.value]; // 3. Optimistically update the UI todos.value.push(payload); try { await $trpc.addTodo.mutate(payload); // 4. Invalidate/Refresh in the background on success await refreshNuxtData(queryKey); } catch { // 5. Rollback on failure todos.value = previousTodos.value; } } </script>- Generating a query key using
Run commands for the tRPC-Nuxt documentation site
mainThe documentation site is built using Astro and Starlight. Use
pnpmto manage the development lifecycle and build process from the project root.pnpm install # Installs dependencies pnpm dev # Starts local dev server at localhost:4321 pnpm build # Build production site to ./dist/ pnpm preview # Preview build locally pnpm astro ... # Run Astro CLI commands (e.g., astro add, astro check)Create context from request headers
mainSince
createContextis called for every incoming request, you can use theH3Eventobject to extract information (like user identity) from request headers. This information is then passed to all tRPC resolvers via thectxobject. For example, you can extract a JWT from theauthorizationheader to populate auserobject in your context.import type { H3Event } from 'h3'; import { decodeAndVerifyJwtToken } from './somewhere/in/your/app/utils'; export async function createTRPCContext(event: H3Event) { // Create your context based on the event object // Will be available as `ctx` in all your resolvers const authorization = getRequestHeader(event, 'authorization'); async function getUserFromHeader() { if (authorization) { const user = await decodeAndVerifyJwtToken(authorization.split(' ')[1]); return user; } return null; } const user = await getUserFromHeader(); return { user, }; } export type Context = Awaited<ReturnType<typeof createTRPCContext>>;Execute tRPC procedures from the server using createCaller()
mainWhen you need to call tRPC procedures directly from the server (for example, within a Nuxt server route or Nitro handler) instead of via a client-side network request, use the
createCaller()method on yourappRouter.createCaller()returns an instance ofRouterCallerwhich allows you to execute queries and mutations as if they were local function calls, bypassing the HTTP layer. This is useful for server-to-server communication or when wrapping tRPC logic inside standard Nuxt API handlers.// Inside a Nuxt server handler (e.g., server/api/example.ts) import { appRouter } from '~/server/trpc/routers'; export default defineEventHandler(async (event) => { // 1. Initialize the caller from your appRouter const caller = appRouter.createCaller({}); // 2. Call the procedure directly const result = await caller.yourProcedureName({ input: 'data' }); return { result }; });