DEV Community

Satoshi
Satoshi

Posted on

Setting Up Clerk Authentication with NestJS and Next.js

Modern web apps need secure, scalable, and developer-friendly authentication. Clerk offers a full-stack authentication solution that works great with Next.js (frontend) and can be securely integrated with a NestJS (backend) server.

In this article, we’ll walk through how to set up Clerk authentication in a fullstack app using Next.js for the UI and NestJS for protected backend APIs.

🔐 What is Clerk?

Clerk is the most comprehensive User Management Platform.

Clerk provides plug-and-play authentication components for:

  • Sign in / Sign up flows
  • User profile management
  • Session handling
  • OAuth and email/password login

It integrates easily into React/Next.js apps and supports token-based API protection for backend services.

🧭 Overview of the Flow

📦 Setup in Next.js (Frontend)

  1. Install Clerk SDK

pnpm add @clerk/nextjs

  1. Add clerkMiddleware() to your app

clerkMiddleware() grants you access to user authentication state throughout your app.

  • Create a middleware.ts file.
    If you’re using the /src directory, create middleware.ts in the /src directory, if not in the root directory.

  • In your middleware.ts file, export the clerkMiddleware() helper:

import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}
Enter fullscreen mode Exit fullscreen mode
  1. Add and Clerk components to your app
import type { Metadata } from 'next'
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

export const metadata: Metadata = {
  title: 'Clerk Next.js Quickstart',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <ClerkProvider
      appearance={{
        cssLayerName: 'clerk',
      }}
    >
      <html lang="en">
        <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
          <header className="flex justify-end items-center p-4 gap-4 h-16">
            <SignedOut>
              <SignInButton />
              <SignUpButton>
                <button className="bg-[#6c47ff] text-white rounded-full font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 cursor-pointer">
                  Sign Up
                </button>
              </SignUpButton>
            </SignedOut>
            <SignedIn>
              <UserButton />
            </SignedIn>
          </header>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode
  1. Get JWT for Backend and call backend API endpoint
const { getToken } = useAuth();
const token = await getToken();
await fetch('/api/protected', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
Enter fullscreen mode Exit fullscreen mode

⚙️ Setup in NestJS (Backend)

  1. Install Dependency
    pnpm add @clerk/backend

  2. Create Clerk Provider

./src/providers/clerk.provider.ts

import { createClerkClient } from '@clerk/backend';
import { ConfigService } from '@nestjs/config';

export const ClerkClientProvider = {
  provide: 'ClerkClient',
  useFactory: (configService: ConfigService) => {
    return createClerkClient({
      publishableKey: configService.get('CLERK_PUBLISHABLE_KEY'),
      secretKey: configService.get('CLERK_SECRET_KEY'),
    });
  },
  inject: [ConfigService],
};
Enter fullscreen mode Exit fullscreen mode
  1. AuthGuard

./src/auth-guard/auth-guard.module.ts

import { Module } from '@nestjs/common';
import { AuthGuardService } from './auth-guard.service';
import { ClerkClientProvider } from 'src/providers/clerk-client.provider';

@Module({
  providers: [
    AuthGuardService,
    ClerkClientProvider
  ],
  exports: ['ClerkClient'],
})
export class AuthGuardModule {}
Enter fullscreen mode Exit fullscreen mode

./src/auth-guard/auth-guard.service.ts

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
  Inject,
} from '@nestjs/common';
import { Request } from 'express';
import { ClerkClient, verifyToken } from '@clerk/backend';

@Injectable()
export class AuthGuardService implements CanActivate {
  constructor(
    @Inject('ClerkClient')
    private readonly clerkClient: ClerkClient,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();

    // Check for session cookie
    const sessionToken = request.cookies.__session;

    // Check for bearer token
    const authHeader = request.headers.authorization;
    const bearerToken = authHeader?.startsWith('Bearer ')
      ? authHeader.substring(7)
      : null;

    if (!sessionToken && !bearerToken) {
      throw new UnauthorizedException('No authentication token provided');
    }

    try {
      // Try to verify the token (either session or bearer)
      const tokenToVerify = bearerToken || sessionToken;
      const tokenPayload = await verifyToken(tokenToVerify, {
        secretKey: process.env.CLERK_SECRET_KEY,
      });

      if (!tokenPayload) {
        throw new UnauthorizedException('Invalid session');
      }

      const user = await this.clerkClient.users.getUser(tokenPayload.sub);
      (request as any).user = user;
      return true;
    } catch (err) {
      console.error('Token verification error:', err);
      throw new UnauthorizedException('Invalid or expired token');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Protect API Routes Using the AuthGuard
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuardService } from './auth/auth.guard';

@Controller('api')
export class ExampleController {
  @UseGuards(AuthGuardService)
  @Get('profile')
  getProfile(@Req() req) {
    return req.user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Clerk + Next.js + NestJS is a powerful trio for building modern, secure, and scalable fullstack applications.

With Clerk, developers benefit from prebuilt UI components, robust session management, and built-in support for OAuth, email/password, and multi-factor authentication. On the frontend, Next.js offers seamless integration with Clerk’s SDK, allowing you to quickly build and protect pages using ClerkProvider and Clerk hooks.

Meanwhile, on the backend, NestJS gives you a modular, testable framework to handle protected routes. By using a custom AuthGuard that verifies JWTs or session cookies issued by Clerk, you can securely validate user identity, enforce authorization rules, and maintain a clean separation of concerns.

Whether you’re building a SaaS platform, an internal admin tool, or a consumer app, this stack helps you ship faster while staying secure.

If you need deeper integration examples, RBAC patterns, SSR protection, or a visual diagram of the full auth flow — feel free to reach out or check the next article!

Thank you.

Top comments (0)