Skip to main content

How to Set Up Clerk Authentication in 5 Minutes

·APIScout Team
clerkauthenticationnextjstutorialapi integration

How to Set Up Clerk Authentication in 5 Minutes

Clerk gives you a complete auth system with pre-built UI components. No building login forms, no managing sessions, no handling password resets. This guide gets you from zero to authenticated in under 5 minutes.

What You'll Build

  • Sign-in and sign-up flows (email, Google, GitHub)
  • Protected routes with middleware
  • User profile management
  • Organization/team support
  • Role-based access control

Prerequisites: Next.js 14+, Clerk account (free: 10,000 MAU).

1. Setup (2 minutes)

Install

npm install @clerk/nextjs

Environment Variables

Create a Clerk application at dashboard.clerk.com, then copy your keys:

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

Add Provider

// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Add Middleware

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isPublicRoute = createRouteMatcher([
  '/',
  '/pricing(.*)',
  '/blog(.*)',
  '/api/webhooks(.*)',
]);

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

That's it. Authentication is now active. Users hitting any non-public route will be redirected to sign in.

2. Auth Components (1 minute)

Sign In / Sign Up Buttons

// components/Header.tsx
import {
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs';

export function Header() {
  return (
    <header>
      <nav>
        <SignedOut>
          <SignInButton mode="modal" />
          <SignUpButton mode="modal" />
        </SignedOut>
        <SignedIn>
          <UserButton afterSignOutUrl="/" />
        </SignedIn>
      </nav>
    </header>
  );
}

The UserButton renders a dropdown with profile, settings, and sign out — no code needed.

Custom Sign-In Page (Optional)

// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';

export default function SignInPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignIn
        appearance={{
          elements: {
            rootBox: 'mx-auto',
            card: 'shadow-xl',
          },
        }}
      />
    </div>
  );
}

Add to env:

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

3. Access User Data

Client-Side

'use client';
import { useUser } from '@clerk/nextjs';

export function Dashboard() {
  const { user, isLoaded } = useUser();

  if (!isLoaded) return <div>Loading...</div>;

  return (
    <div>
      <h1>Welcome, {user?.firstName}!</h1>
      <p>Email: {user?.primaryEmailAddress?.emailAddress}</p>
      <img src={user?.imageUrl} alt="Profile" width={64} />
    </div>
  );
}

Server-Side

// app/dashboard/page.tsx
import { currentUser } from '@clerk/nextjs/server';

export default async function DashboardPage() {
  const user = await currentUser();

  if (!user) return <div>Not signed in</div>;

  return (
    <div>
      <h1>Dashboard</h1>
      <p>User ID: {user.id}</p>
      <p>Email: {user.emailAddresses[0]?.emailAddress}</p>
    </div>
  );
}

In API Routes

// app/api/user/route.ts
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId } = await auth();

  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Fetch user-specific data
  const data = await getUserData(userId);
  return NextResponse.json(data);
}

4. Social Login

Enable Providers

In Clerk Dashboard → User & Authentication → Social Connections:

  • Toggle on: Google, GitHub, Apple, etc.
  • Add OAuth credentials for each provider

They appear automatically in your sign-in/sign-up components. No code changes needed.

5. Organizations

Enable Organizations

In Clerk Dashboard → Organizations → Enable.

Organization Switcher

import { OrganizationSwitcher } from '@clerk/nextjs';

export function OrgSwitcher() {
  return (
    <OrganizationSwitcher
      afterCreateOrganizationUrl="/dashboard"
      afterSelectOrganizationUrl="/dashboard"
    />
  );
}

Check Organization in API

import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { userId, orgId, orgRole } = await auth();

  if (!orgId) {
    return NextResponse.json({ error: 'No organization selected' }, { status: 400 });
  }

  // orgRole: 'org:admin' | 'org:member'
  if (orgRole !== 'org:admin') {
    return NextResponse.json({ error: 'Admin only' }, { status: 403 });
  }

  return NextResponse.json({ orgId, role: orgRole });
}

6. Webhooks

Sync Clerk events with your database:

Set Up Webhook

In Clerk Dashboard → Webhooks → Add Endpoint:

  • URL: https://your-app.com/api/webhooks/clerk
  • Events: user.created, user.updated, user.deleted

Handle Events

// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';

export async function POST(req: Request) {
  const body = await req.text();
  const headerPayload = headers();

  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);

  let evt: WebhookEvent;
  try {
    evt = wh.verify(body, {
      'svix-id': headerPayload.get('svix-id')!,
      'svix-timestamp': headerPayload.get('svix-timestamp')!,
      'svix-signature': headerPayload.get('svix-signature')!,
    }) as WebhookEvent;
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (evt.type) {
    case 'user.created':
      await createUserInDB({
        clerkId: evt.data.id,
        email: evt.data.email_addresses[0]?.email_address,
        name: `${evt.data.first_name} ${evt.data.last_name}`,
      });
      break;

    case 'user.updated':
      await updateUserInDB(evt.data.id, evt.data);
      break;

    case 'user.deleted':
      await deleteUserFromDB(evt.data.id!);
      break;
  }

  return new Response('OK');
}

7. Theming

// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider
      appearance={{
        baseTheme: dark,
        variables: {
          colorPrimary: '#2563eb',
          borderRadius: '0.5rem',
        },
        elements: {
          card: 'shadow-lg',
          formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
        },
      }}
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Pricing

PlanMAUPrice
Free10,000$0
Pro10,000+$25/month + $0.02/MAU over 10K
EnterpriseCustomCustom

Free tier includes: unlimited social logins, organizations, custom domains.

Common Mistakes

MistakeImpactFix
Forgetting middlewareNo routes are protectedAlways add middleware.ts
Not syncing with databaseCan't query users in your DBSet up webhooks for user events
Client-side only auth checksSecurity bypassUse auth() server-side for sensitive routes
Not handling org contextMulti-tenant bugsCheck orgId in org-scoped routes
Exposing secret keyAccount compromiseOnly NEXT_PUBLIC_ key goes to client

Choosing auth? Compare Clerk vs Auth0 vs Firebase Auth on APIScout — pricing, DX, and feature comparison.

Comments