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

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!

Sign up to receive updates on new content & exclusive offers

We don’t spam! Cancel anytime.