Next.js and NestJS Authentication with Clerk
Frontend : Next.js with Clerk (port 3000)
Backend : NestJS (port 4000)
Authentication : Clerk + Token verification
Database : Prisma as ORM
sequenceDiagram
participant Browser
participant NextClient as Next.js Client
participant NextServer as Next.js Server
participant Clerk
participant NestJS
participant Prisma
%% Flow 1: Login Flow
rect rgb(200, 220, 240)
Note over Browser,NestJS: Flow 1: Login Process
Browser->>NextServer: Access /sign-in
NextServer->>Browser: Return Clerk sign-in page
Browser->>Clerk: Submit credentials
Clerk->>Clerk: Validate credentials
Clerk-->>Browser: Set session cookie
Clerk-->>Browser: Redirect to /products
end
%% Flow 2: Protected Page Access
rect rgb(220, 240, 200)
Note over Browser,NestJS: Flow 2: Protected Page Access (e.g., Cart)
Browser->>+NextServer: Access /cart
NextServer->>Clerk: Verify session & get token
Clerk-->>NextServer: Return token
NextServer->>+NestJS: Fetch cart data with token
Note over NextServer,NestJS: Authorization: Bearer {token}
NestJS->>NestJS: Validate token via ClerkAuthGuard
NestJS->>Prisma: Query cart data
Prisma-->>NestJS: Return cart data
NestJS-->>-NextServer: Return cart data
NextServer-->>-Browser: Return rendered page
end
%% Flow 3: Client-side API Calls
rect rgb(240, 220, 200)
Note over Browser,NestJS: Flow 3: Client-side API Calls (e.g., Add to Cart)
Browser->>NextClient: Click "Add to Cart"
NextClient->>Clerk: Get token via useAuthToken
Clerk-->>NextClient: Return token
NextClient->>+NestJS: POST /cart/items with token
Note over NextClient,NestJS: Authorization: Bearer {token}
NestJS->>NestJS: Validate token via ClerkAuthGuard
NestJS->>Prisma: Update cart in database
Prisma-->>NestJS: Return updated cart
NestJS-->>-NextClient: Return updated cart
NextClient->>Browser: Update UI with toast notification
end
Loading
3. Frontend Setup (Next.js)
{
"@clerk/nextjs" : " ^6.9.12" ,
"next" : " 15.1.4" ,
"react" : " ^19.0.0"
}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = pk_test_****
CLERK_SECRET_KEY = sk_test_****
// app/layout.tsx
import { ClerkProvider , SignedIn , SignedOut , SignInButton , UserButton } from '@clerk/nextjs'
export default function RootLayout ( {
children,
} : {
children : React . ReactNode
} ) {
return (
< ClerkProvider >
< html lang = "en" >
< body >
< SignedOut >
< SignInButton / >
< / SignedOut >
< SignedIn >
< UserButton / >
< / SignedIn >
{ children}
< ToastProvider / >
< / body >
< / html >
< / ClerkProvider >
)
}
Authentication Middleware
// middleware.ts
import { clerkMiddleware , createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher ( [
'/sign-in(.*)' ,
'/sign-up(.*)' ,
'/' ,
'/products' ,
] )
export default clerkMiddleware ( async ( auth , request ) => {
if ( ! isPublicRoute ( request ) ) {
await auth . protect ( )
}
} )
// hooks/use-auth-token.ts
export const useAuthToken = ( ) => {
const { getToken, isSignedIn } = useAuth ( )
const getAuthToken = async ( ) => {
if ( ! isSignedIn ) return null
return getToken ( )
}
return {
getAuthToken,
isSignedIn,
}
}
// app/cart/page.tsx
async function getCart ( ) : Promise < Cart > {
const { getToken } = await auth ( )
const token = await getToken ( )
if ( ! token ) {
redirect ( '/sign-in' )
}
const response = await fetch ( 'http://localhost:4000/cart' , {
headers : {
Authorization : `Bearer ${ token } ` ,
} ,
cache : 'no-store' ,
} )
return response . json ( )
}
4. Backend Setup (NestJS)
{
"@clerk/backend" : " latest" ,
"@nestjs/common" : " latest" ,
"@nestjs/passport" : " latest"
}
// libs/auth/src/clerk-auth.guard.ts
@Injectable ( )
export class ClerkAuthGuard extends AuthGuard ( 'clerk' ) {
constructor ( private reflector : Reflector ) {
super ( )
}
override canActivate ( context : ExecutionContext ) {
const isPublic = this . reflector . getAllAndOverride < boolean > ( IS_PUBLIC_KEY , [
context . getHandler ( ) ,
context . getClass ( ) ,
] )
if ( isPublic ) {
return true
}
return super . canActivate ( context )
}
}
// libs/auth/src/clerk.strategy.ts
@Injectable ( )
export class ClerkStrategy extends PassportStrategy ( Strategy , 'clerk' ) {
constructor (
@Inject ( 'ClerkClient' )
private readonly clerkClient : ClerkClient ,
private readonly configService : ConfigService
) {
super ( )
}
async validate ( req : Request ) : Promise < User > {
const token = req . headers . authorization ?. split ( ' ' ) . pop ( )
if ( ! token ) {
throw new UnauthorizedException ( 'No token provided' )
}
try {
const tokenPayload = await verifyToken ( token , {
secretKey : this . configService . get ( 'CLERK_SECRET_KEY' ) ,
} )
const user = await this . clerkClient . users . getUser ( tokenPayload . sub )
return user
} catch ( error ) {
throw new UnauthorizedException ( 'Invalid token' )
}
}
}
Protected Controller Example
// apps/api/src/controllers/cart.controller.ts
@Controller ( 'cart' )
@UseGuards ( ClerkAuthGuard )
export class CartController {
constructor ( private readonly cartService : CartService ) { }
@Get ( )
async getMyCart ( @CurrentUser ( ) user : User ) {
return this . cartService . findUserCart ( user . id )
}
@Post ( 'items' )
async addToCart ( @CurrentUser ( ) user : User , @Body ( ) data : { productId : number ; quantity : number } ) {
return this . cartService . addItem ( user . id , data . productId , data . quantity )
}
}
5. Security Considerations
Clerk handles token lifecycle management
Tokens are stored securely in httpOnly cookies
Auto refresh token mechanism
No client-side token storage
All protected routes are guarded by Clerk middleware
Backend validates tokens on every request
Public decorator for explicitly marking public routes
CORS enabled with proper configuration
Environment variables for sensitive data
Error handling with proper HTTP status codes
// apps/api/src/main.ts
app . enableCors ( {
origin : '*' ,
credentials : true ,
} )
// lib/cart.ts
export async function addToCart ( token : string | null , productId : number , quantity : number ) {
if ( ! token ) {
throw new Error ( 'Not authenticated' )
}
const response = await fetch ( `${ API_URL } /cart/items` , {
method : 'POST' ,
headers : {
Authorization : `Bearer ${ token } ` ,
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( { productId, quantity } ) ,
} )
return response . json ( )
}
// app/products/page.tsx
export default function ProductsPage ( ) {
const { getAuthToken, isSignedIn } = useAuthToken ( )
const { toast } = useToast ( )
const handleAddToCart = async ( productId : number ) => {
if ( ! isSignedIn ) {
toast ( {
title : 'Please sign in' ,
description : 'You need to be signed in to add items to your cart' ,
variant : 'destructive' ,
} )
return
}
try {
const token = await getAuthToken ( )
await addToCart ( token , productId , 1 )
toast ( {
title : 'Added to cart' ,
description : 'Item has been added to your cart' ,
} )
} catch ( error ) {
toast ( {
title : 'Error' ,
description : 'Failed to add item to cart' ,
variant : 'destructive' ,
} )
}
}
}