Clerk Next.js
Authentication Library
Clerk Next.js
Overview
Clerk Next.js is a modern authentication and user management library designed specifically for Next.js applications. As of 2025, it provides full support for both App Router and Pages Router, and is widely adopted as the most developer-friendly authentication solution. It offers comprehensive authentication features including JWT, OAuth 2.0, session management, and multi-factor authentication (MFA), allowing developers to implement a complete user authentication system with just a few lines of code. It's fully integrated with React's hook patterns and provides intuitive APIs such as useAuth, useUser, and useClerk.
Details
Clerk Next.js is a library that comprehensively provides authentication features required by modern web applications. Key features include:
- Next.js Optimization: Complete integration with App Router, Pages Router, API Routes, and middleware
- JWT Integration: Automatic JWT token management and session claim processing
- OAuth Providers: Support for 20+ external authentication providers including Google, GitHub, Discord, Apple, Microsoft
- Session Management: Unified session processing on both server-side and client-side
- Reactive Hooks: Declarative authentication state management with
useAuth,useUser, anduseClerk - Protected Routes: Automatic route protection and redirect functionality based on middleware
- Customizable: Complete customization of authentication flows, UI components, and redirect destinations
Pros and Cons
Pros
- Seamless integration and high performance through Next.js-specific optimization
- Dramatic improvement in development speed with complete authentication system in just a few lines of code
- Enterprise-level security features including JWT, OAuth, MFA, and RBAC
- Intuitive and declarative API design through React hooks patterns
- Consistent development experience across both App Router and Pages Router
- Rich documentation and active community support
Cons
- Cannot be used with frameworks other than Next.js (not portable to React, Vue, Angular, etc.)
- Subject to impact when Clerk service is down due to external service dependency
- Constraints within Clerk's ecosystem for advanced customization
- Potentially high usage fees for large-scale applications
- Risk of vendor lock-in when migrating from proprietary authentication systems
Reference Pages
- Clerk Official Site
- Clerk Next.js Documentation
- GitHub - clerk/javascript
- Clerk Next.js API Reference
Code Examples
Basic Setup and Installation
# Install Clerk Next.js package
npm install @clerk/nextjs
# Environment variable setup (.env.local)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# Optional: Redirect URL configuration
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
Basic Authentication Setup with App Router
// app/layout.tsx - Wrap entire app with ClerkProvider
import { ClerkProvider } from '@clerk/nextjs'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
// middleware.ts - Route protection configuration
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/admin(.*)',
'/profile(.*)'
])
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/public(.*)'
])
export default clerkMiddleware((auth, request) => {
// Skip authentication for public routes
if (isPublicRoute(request)) {
return NextResponse.next()
}
// Require authentication for protected routes
if (isProtectedRoute(request)) {
auth().protect()
}
return NextResponse.next()
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)']
}
Sign-in and Sign-up Page Implementation
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignIn
appearance={{
elements: {
formButtonPrimary: 'bg-blue-600 hover:bg-blue-700 text-white',
card: 'shadow-lg'
}
}}
routing="hash"
signUpUrl="/sign-up"
redirectUrl="/dashboard"
/>
</div>
)
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'
export default function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignUp
appearance={{
elements: {
formButtonPrimary: 'bg-green-600 hover:bg-green-700 text-white',
card: 'shadow-lg'
}
}}
routing="hash"
signInUrl="/sign-in"
redirectUrl="/dashboard"
/>
</div>
)
}
Conditional Rendering Based on Authentication State
// app/page.tsx - Authentication state display on homepage
import {
SignedIn,
SignedOut,
SignInButton,
UserButton,
useUser
} from '@clerk/nextjs'
import Link from 'next/link'
export default function HomePage() {
return (
<div className="container mx-auto px-4 py-8">
<nav className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold">My App</h1>
<div className="flex items-center space-x-4">
<SignedOut>
<SignInButton mode="modal">
<button className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Sign In
</button>
</SignInButton>
<Link href="/sign-up">
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
Sign Up
</button>
</Link>
</SignedOut>
<SignedIn>
<Link href="/dashboard">
<button className="text-blue-600 hover:text-blue-800">
Dashboard
</button>
</Link>
<UserButton
afterSignOutUrl="/"
appearance={{
elements: {
avatarBox: 'w-10 h-10'
}
}}
/>
</SignedIn>
</div>
</nav>
<main>
<SignedOut>
<div className="text-center">
<h2 className="text-4xl font-bold mb-4">Welcome to My App</h2>
<p className="text-xl text-gray-600 mb-8">
Sign in to get started
</p>
</div>
</SignedOut>
<SignedIn>
<WelcomeMessage />
</SignedIn>
</main>
</div>
)
}
function WelcomeMessage() {
const { user } = useUser()
return (
<div className="text-center">
<h2 className="text-4xl font-bold mb-4">
Hello, {user?.firstName || user?.username}!
</h2>
<p className="text-xl text-gray-600">
Authentication completed. Enjoy the app.
</p>
</div>
)
}
Protected Dashboard Page
// app/dashboard/page.tsx - Authentication-required page
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
import UserProfile from '@/components/UserProfile'
import DashboardStats from '@/components/DashboardStats'
export default async function DashboardPage() {
// Server-side authentication check
const { userId } = await auth()
if (!userId) {
redirect('/sign-in')
}
// Get current user information
const user = await currentUser()
if (!user) {
redirect('/sign-in')
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Dashboard</h1>
<p className="text-gray-600">
Welcome, {user.firstName || user.username}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<UserProfile user={user} />
<DashboardStats userId={userId} />
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Account Information</h3>
<div className="space-y-2">
<p><strong>Email:</strong> {user.primaryEmailAddress?.emailAddress}</p>
<p><strong>Sign-up Date:</strong> {user.createdAt?.toLocaleDateString('en-US')}</p>
<p><strong>Last Sign-in:</strong> {user.lastSignInAt?.toLocaleDateString('en-US')}</p>
</div>
</div>
</div>
</div>
)
}
API Routes with Authentication and JWT Processing
// app/api/profile/route.ts - Protected API
import { auth, currentUser } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
try {
// Get authentication state and JWT claims
const { userId, sessionClaims, getToken } = await auth()
if (!userId) {
return new NextResponse(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401 }
)
}
// Get JWT token (for external API calls)
const token = await getToken()
// Get current user information
const user = await currentUser()
// Detailed session claims information
const userMetadata = {
userId: userId,
sessionId: sessionClaims?.sid,
email: user?.primaryEmailAddress?.emailAddress,
roles: sessionClaims?.metadata?.roles || [],
permissions: sessionClaims?.metadata?.permissions || []
}
return NextResponse.json({
success: true,
user: userMetadata,
tokenAvailable: !!token
})
} catch (error) {
console.error('API error:', error)
return new NextResponse(
JSON.stringify({ error: 'Server error' }),
{ status: 500 }
)
}
}
export async function PUT(request: Request) {
try {
const { userId } = await auth()
if (!userId) {
return new NextResponse(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401 }
)
}
const updateData = await request.json()
// Profile update logic
// userService.updateProfile(userId, updateData)
return NextResponse.json({
success: true,
message: 'Profile updated successfully'
})
} catch (error) {
return new NextResponse(
JSON.stringify({ error: 'Profile update failed' }),
{ status: 500 }
)
}
}
External API Authentication and Token Management
// components/ExternalDataFetcher.tsx - External API integration
'use client'
import { useAuth } from '@clerk/nextjs'
import { useState, useEffect } from 'react'
interface ExternalApiData {
id: string
name: string
data: any
}
export default function ExternalDataFetcher() {
const { getToken, isLoaded, isSignedIn } = useAuth()
const [data, setData] = useState<ExternalApiData[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchExternalData = async () => {
try {
setLoading(true)
setError(null)
// Get JWT token from Clerk
const token = await getToken()
if (!token) {
throw new Error('Unable to get authentication token')
}
// Request to external API using JWT token
const response = await fetch('/api/external-data', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`API Error: ${response.status}`)
}
const result = await response.json()
setData(result.data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
console.error('External API fetch error:', err)
} finally {
setLoading(false)
}
}
// Auto-fetch on mount
useEffect(() => {
if (isLoaded && isSignedIn) {
fetchExternalData()
}
}, [isLoaded, isSignedIn])
if (!isLoaded) {
return <div className="animate-pulse">Loading...</div>
}
if (!isSignedIn) {
return <div className="text-red-600">Authentication required</div>
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">External Data</h3>
<button
onClick={fetchExternalData}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Updating...' : 'Refresh'}
</button>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
Error: {error}
</div>
)}
<div className="grid gap-4">
{data.map((item) => (
<div key={item.id} className="bg-white p-4 rounded-lg shadow">
<h4 className="font-medium">{item.name}</h4>
<pre className="mt-2 text-sm text-gray-600">
{JSON.stringify(item.data, null, 2)}
</pre>
</div>
))}
</div>
{data.length === 0 && !loading && !error && (
<div className="text-center text-gray-500 py-8">
No data available
</div>
)}
</div>
)
}
Advanced Authentication Hooks and Customization
// hooks/useAuthStatus.ts - Custom authentication hook
import { useAuth, useUser, useClerk } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import { useCallback } from 'react'
export function useAuthStatus() {
const {
isLoaded: authLoaded,
isSignedIn,
userId,
sessionId,
getToken
} = useAuth()
const {
isLoaded: userLoaded,
user
} = useUser()
const { signOut, openSignIn } = useClerk()
const router = useRouter()
// Custom sign-out processing
const handleSignOut = useCallback(async () => {
try {
await signOut()
router.push('/')
} catch (error) {
console.error('Sign out error:', error)
}
}, [signOut, router])
// Token acquisition and refresh
const getAuthToken = useCallback(async () => {
try {
return await getToken()
} catch (error) {
console.error('Token fetch error:', error)
return null
}
}, [getToken])
// Authentication-required action
const requireAuth = useCallback((action: () => void) => {
if (!isSignedIn) {
openSignIn()
return
}
action()
}, [isSignedIn, openSignIn])
return {
// Basic state
isLoaded: authLoaded && userLoaded,
isSignedIn,
user,
userId,
sessionId,
// Extended functionality
getAuthToken,
handleSignOut,
requireAuth,
// User information
userEmail: user?.primaryEmailAddress?.emailAddress,
userName: user?.firstName || user?.username || 'Unknown',
userRoles: user?.publicMetadata?.roles as string[] || [],
// State checks
isAdmin: (user?.publicMetadata?.roles as string[])?.includes('admin') || false,
isVerified: user?.primaryEmailAddress?.verification?.status === 'verified'
}
}
// components/AuthGuard.tsx - Authentication guard component
import { ReactNode } from 'react'
import { useAuthStatus } from '@/hooks/useAuthStatus'
import { SignInButton } from '@clerk/nextjs'
interface AuthGuardProps {
children: ReactNode
fallback?: ReactNode
requireAdmin?: boolean
requireVerified?: boolean
}
export default function AuthGuard({
children,
fallback,
requireAdmin = false,
requireVerified = false
}: AuthGuardProps) {
const {
isLoaded,
isSignedIn,
isAdmin,
isVerified
} = useAuthStatus()
// Loading state
if (!isLoaded) {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
// Unauthenticated
if (!isSignedIn) {
return fallback || (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<h2 className="text-2xl font-bold">Authentication Required</h2>
<p className="text-gray-600">Please sign in to access this page</p>
<SignInButton mode="modal">
<button className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
Sign In
</button>
</SignInButton>
</div>
)
}
// Admin privilege check
if (requireAdmin && !isAdmin) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<h2 className="text-2xl font-bold text-red-600">Access Denied</h2>
<p className="text-gray-600">You don't have permission to access this page</p>
</div>
)
}
// Email verification check
if (requireVerified && !isVerified) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<h2 className="text-2xl font-bold text-yellow-600">Email Verification Required</h2>
<p className="text-gray-600">Please verify your email address to access this page</p>
</div>
)
}
return <>{children}</>
}