AiTechWorlds
AiTechWorlds
Middleware in Next.js is code that runs before a request reaches any page, making it the fastest place to check authentication for protected routes.
Think of an office building with a receptionist checking badges at the front door (middleware) and a second badge scanner outside the executive floor (layout-level checks). The lobby check catches most unauthorized visitors instantly; the second check exists because the lobby guard's list can occasionally be a few seconds out of date. Protecting routes in Next.js works the same way — fast edge-level middleware checks, backed by server-side auth verification in layouts and pages.
Middleware runs before any page is rendered. It's the ideal place to check authentication for every request to protected routes, before any database query or server-side rendering happens.
// src/middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export default auth(function middleware(req) {
const { nextUrl, auth: session } = req;
const isLoggedIn = !!session;
// Define protected route patterns
const isProtected = nextUrl.pathname.startsWith("/dashboard") ||
nextUrl.pathname.startsWith("/settings") ||
nextUrl.pathname.startsWith("/api/protected");
const isAuthPage = nextUrl.pathname.startsWith("/login") ||
nextUrl.pathname.startsWith("/register");
// Redirect unauthenticated users from protected routes
if (isProtected && !isLoggedIn) {
const redirectUrl = new URL("/login", nextUrl.origin);
redirectUrl.searchParams.set("callbackUrl", nextUrl.pathname);
return NextResponse.redirect(redirectUrl);
}
// Redirect authenticated users away from auth pages
if (isAuthPage && isLoggedIn) {
return NextResponse.redirect(new URL("/dashboard", nextUrl.origin));
}
return NextResponse.next();
});
// Tell Next.js which routes to run middleware on
export const config = {
matcher: [
// Match all routes except static files and Next.js internals
"/((?!_next/static|_next/image|favicon.ico|public/).*)",
],
};// src/middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export default auth(function middleware(req) {
const session = req.auth;
const { pathname } = req.nextUrl;
// Admin-only routes
if (pathname.startsWith("/admin")) {
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
if (session.user.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
}
// Instructor-only routes
if (pathname.startsWith("/studio")) {
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
if (!["admin", "instructor"].includes(session.user.role)) {
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
}
// Regular protected routes
if (pathname.startsWith("/dashboard")) {
if (!session) {
const url = new URL("/login", req.url);
url.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(url);
}
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!_next|api/auth|_vercel|.*\\..*).*)", "/api/:path*"],
};Middleware is the first check. Layouts provide a second layer — useful when the layout itself needs auth data to render correctly:
// src/app/dashboard/layout.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
// Double-check auth in the layout (middleware might have race conditions during deploy)
if (!session) {
redirect("/login");
}
return (
<div className="flex min-h-screen">
<DashboardSidebar user={session.user} />
<main className="flex-1 bg-gray-50">{children}</main>
</div>
);
}This is important: middleware runs at the edge and might have stale session data during Vercel deployment rollovers. The layout check ensures correctness.
For pages that need fine-grained checks beyond "is logged in":
// src/app/courses/[slug]/edit/page.tsx
import { auth } from "@/auth";
import { redirect, notFound } from "next/navigation";
import { db } from "@/lib/db";
export default async function EditCoursePage({ params }: { params: { slug: string } }) {
const session = await auth();
if (!session) redirect("/login");
const course = await db.course.findUnique({
where: { slug: params.slug },
select: { id: true, title: true, authorId: true },
});
if (!course) notFound();
// Only the author or an admin can edit
if (course.authorId !== session.user.id && session.user.role !== "admin") {
redirect("/unauthorized");
}
return <CourseEditor courseId={course.id} />;
}For API routes in the App Router:
// src/app/api/courses/[id]/route.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const course = await db.course.findUnique({ where: { id: params.id } });
if (!course) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
if (course.authorId !== session.user.id && session.user.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
await db.course.delete({ where: { id: params.id } });
return new Response(null, { status: 204 });
}After login, redirect users back to what they were trying to access:
// src/app/login/page.tsx
"use client";
import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
export default function LoginPage() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
callbackUrl, // redirect to original destination after login
});
}
return (
<form onSubmit={handleLogin}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign in</button>
</form>
);
}User tries to visit /dashboard/settings → Middleware redirects to /login?callbackUrl=/dashboard/settings → User logs in → Redirects to /dashboard/settings.
// src/app/unauthorized/page.tsx
import Link from "next/link";
export default function UnauthorizedPage() {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">Access Denied</h1>
<p className="text-gray-500 mt-3">
You don't have permission to access this page.
</p>
<div className="flex gap-3 justify-center mt-6">
<Link href="/dashboard" className="text-blue-600 hover:underline">
Go to Dashboard
</Link>
<span className="text-gray-300">|</span>
<Link href="/" className="text-blue-600 hover:underline">
Go Home
</Link>
</div>
</div>
</main>
);
}Next lesson: Tailwind CSS in Next.js — typography plugin, advanced theming, and component patterns.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises