When you’re building a modern API with NestJS and tRPC, authentication is usually one of the first hurdles. Instead of rolling your own, you can use Better Auth, a lightweight authentication service that integrates seamlessly with NestJS. In this post, I’ll show you how to wire it up with NestJS tRPC so that every tRPC call can automatically include the authenticated user.
Getting Started
Before getting started, you can check out my previous videos on both integrating tRPC + NestJS/Next.js and Better Auth + NestJS
- Better Auth + NestJS
- NestJS + tRPC
1. Install the Dependencies
First, add the required packages:
pnpm add @mguay/nestjs-better-auth nestjs-trpc
2. Configure the TRPC Module
The TRPC module is where you bootstrap tRPC inside NestJS. We’ll set the base path for our API and tell it which AppContext
to use:
TRPCModule.forRoot({
autoSchemaFile: '../../packages/trpc/src/server',
basePath: '/api/trpc',
context: AppContext
});
We’re setting a basePath here because later on, we’ll need to make sure we’re proxying requests from our Next.js server to avoid CORs errors and critically, make sure our secure session cookie from Better Auth is actually passed, since by default it won’t be passed if we’re running our backend & UI on different domains.
See: https://web.dev/articles/samesite-cookies-explained
3. Provide Request/Response in the Context
Your context is where you can pass anything you want into tRPC resolvers and middleware. Here we expose the raw Express req
and res
so that Better Auth can access the session cookie we set when authenticating our users.
import { Injectable } from '@nestjs/common';
import { ContextOptions, TRPCContext } from 'nestjs-trpc';
@Injectable()
export class AppContext implements TRPCContext {
async create(opts: ContextOptions): Promise<Record<string, unknown>> {
return {
req: opts.req,
res: opts.res,
};
}
}
4. Create an Auth Middleware
This middleware runs on every tRPC request. It asks Better Auth to resolve a session from the cookies in the request headers. If a valid session is found, the user
and session
are attached to the tRPC context.
import { TRPCMiddleware, MiddlewareOptions } from 'nestjs-trpc';
import { Injectable } from '@nestjs/common';
import { AuthService } from '@mguay/nestjs-better-auth';
@Injectable()
export class AuthMiddleware implements TRPCMiddleware {
constructor(private readonly authService: AuthService) {}
async use(opts: MiddlewareOptions<{ req: any; res: any }>) {
const { next, ctx } = opts;
try {
// Extract session from request (Better Auth parses cookies automatically)
const session = await this.authService.api.getSession({
headers: ctx.req.headers,
});
// Add user and session to context if authenticated
if (session?.user && session?.session) {
return next({
ctx: {
...ctx,
user: session.user,
session: session.session,
},
});
}
throw new Error('Unauthorized');
} catch {
throw new Error('Unauthorized');
}
}
}
Here, any request without a valid session will throw an Unauthorized
error. If you want to allow anonymous access for some routes, you can instead not throw, and just continue with user: undefined
.
5. Apply the Middleware
@UseMiddlewares(AuthMiddleware)
@Query({ output: z.array(postResponseSchema) })
async findAll(@Ctx() context: any) {
console.log('Finding all posts', context.user, context.session);
return this.postsService.findAll();
}
Here we’re applying our middleware to a tRPC router so that it actually runs before executing the procedure and ensures the caller has a valid Better Auth session cookie.
We’re also extracting the context object that we created earlier by using the @Ctx decorator. From here we can pull off the user and session objects and access them during the procedure, which is especially helpful.
5. Setup the Client
On the client (Next.js in this case), we configure a tRPC client that points to our NestJS API via a rewrite. The client uses httpBatchLink
to send batched queries/mutations.
import {
createTRPCReact,
CreateTRPCReact,
httpBatchLink,
} from '@trpc/react-query';
import { AppRouter } from '@repo/trpc/router';
import { QueryClient } from '@tanstack/react-query';
export const trpc: CreateTRPCReact<AppRouter, object> = createTRPCReact<
AppRouter,
object
>();
export const queryClient = new QueryClient();
export const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
});
In next.config.js
, we forward /api
requests to our backend API. Again, this is a critical step to ensure the session cookie is passed to the backend, because since we’re using Secure HTTP-only cookies, they won’t be passed for cross-domain requests.
const nextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: `${process.env.API_URL}/api/:path*`,
},
];
},
};
export default nextConfig;
6. Wrap the App with a Provider
Finally, wrap your React tree with a TrpcProvider
that wires up both tRPC and React Query.
'use client';
import { PropsWithChildren } from 'react';
import { queryClient, trpc, trpcClient } from '../lib/trpc-client';
import { QueryClientProvider } from '@tanstack/react-query';
export default function TrpcProvider({ children }: PropsWithChildren) {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
}
Then in your layout:
<TrpcProvider>{children}</TrpcProvider>
Now you can call your queries anywhere in the app:
const posts = trpc.postsRouter.findAll.useQuery();
Assuming that you have logged in using Better Auth and have a valid session cookie, you should the log in the backend showing us the current user context session and user object.
Without the cookie, the reques will fail and we now have full authentication integrated with Better Auth and NestJS tRPC.

Conclusion
With this setup:
- Better-Auth handles authentication and cookie parsing.
- NestJS + tRPC propagates the authenticated
user
into every resolver. - Your client can call
trpc
hooks and automatically include authentication context.
This gives you a clean, type-safe, and consistent way to protect your tRPC procedures while still working seamlessly with Next.js on the frontend.
👉 That’s it — you now have Better-Auth integrated with NestJS + tRPC. From here, you can extend the middleware with role checks, session expiration handling, or custom error messages.
🚀 Ready to level up your full-stack skills?
This example comes directly from my new course: Build an Instagram Clone with NestJS & Next.js – Fullstack Microservices Mastery.
In this hands-on course, you’ll build a modern Instagram clone from scratch using NestJS, Next.js, tRPC, shadcn/ui, Turborepo, and Better Auth. We’ll cover everything from authentication and real-time feed updates to likes, comments, stories, and a production-ready microservices architecture.
You’ll learn how to:
- Structure scalable apps with a clean architecture
- Share code between backend and frontend in a monorepo
- Ship polished features with a modern UI and great developer experience
👉 Enroll today and start building with the latest fullstack tech stack!