Authentication is usually the most time-consuming part of any app… but it doesn’t have to be.
With Clerk, you can add a fully secure auth system to Next.js (App Router) in minutes — and in this guide, we’ll build a custom Sign-In / Sign-Up UI plus a protected dashboard route.
🚀 What we'll build
- 🔐 Sign In page (custom UI)
- 🆕 Sign Up page (custom UI)
- 🛡 Protected route:
/dashboard(only logged-in users can open it) - ⚡ Using Clerk hooks (
useSignIn,useSignUp,useUser)
1️⃣ Install Clerk in Next.js (App Router)
npm install @clerk/nextjs
In .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXXX
CLERK_SECRET_KEY=sk_test_XXXXXXXX
2️⃣ Wrap the app with ClerkProvider — app/layout.tsx
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>
);
}
3️⃣ Build custom Sign-In page — app/sign-in/page.tsx
"use client";
import { useSignIn } from "@clerk/nextjs";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignInPage() {
const { isLoaded, signIn } = useSignIn();
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
if (!isLoaded) return null;
const handleSubmit = async (e: any) => {
e.preventDefault();
const res = await signIn.create({ identifier: email, password });
if (res.status === "complete") router.push("/dashboard");
};
return (
<form onSubmit={handleSubmit} className="max-w-sm mx-auto mt-20 flex flex-col gap-3">
<h2 className="text-2xl font-semibold text-center">Sign In</h2>
<input className="border px-3 py-2 rounded" placeholder="Email" onChange={e => setEmail(e.target.value)} />
<input className="border px-3 py-2 rounded" placeholder="Password" type="password" onChange={e => setPassword(e.target.value)} />
<button className="bg-black text-white py-2 rounded">Continue</button>
</form>
);
}
4️⃣ Build custom Sign-Up page — app/sign-up/page.tsx
"use client";
import { useSignUp } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function SignUpPage() {
const { isLoaded, signUp } = useSignUp();
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
if (!isLoaded) return null;
const handleSubmit = async (e: any) => {
e.preventDefault();
const res = await signUp.create({ emailAddress: email, password });
if (res.status === "complete") router.push("/dashboard");
};
return (
<form onSubmit={handleSubmit} className="max-w-sm mx-auto mt-20 flex flex-col gap-3">
<h2 className="text-2xl font-semibold text-center">Create Account</h2>
<input className="border px-3 py-2 rounded" placeholder="Email" onChange={e => setEmail(e.target.value)} />
<input className="border px-3 py-2 rounded" placeholder="Password" type="password" onChange={e => setPassword(e.target.value)} />
<button className="bg-black text-white py-2 rounded">Sign Up</button>
</form>
);
}
5️⃣ Create the protected Dashboard route — app/dashboard/page.tsx
import { auth, UserButton } from "@clerk/nextjs";
export default function DashboardPage() {
const { userId } = auth();
if (!userId) return <div className="text-center mt-20">Unauthorized</div>;
return (
<div className="max-w-xl mx-auto mt-20 text-center">
<UserButton afterSignOutUrl="/" />
<h1 className="text-3xl font-bold mt-6">Welcome to Dashboard 🎉</h1>
</div>
);
}
6️⃣ Protect the route with middleware — middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtectedRoute = createRouteMatcher(["/dashboard(.*)"]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
});
export const config = {
matcher: ["/((?!_next|.*\\..*).*)"],
};
🎉 Done!
You now have:
- 🔐 Custom Sign-In page
- 🆕 Custom Sign-Up page
- 🛡 Protected Dashboard route
- 🚫 Automatic access blocking via middleware
Top comments (0)