
Mastering Authentication in Next.js 15: The Senior Engineer's Guide
1. The Architecture: "One Config to Rule Them All"
In NextAuth v4, configuration was often trapped inside an API route. In v5, we lift the config into a standalone auth.ts file. This allows us to use the same logic in Middleware, Server Actions, and API routes.
Step 1: Initialize the Auth Engine
Create a file at your root (or /src) called auth.ts. This is the brain of your auth system.
TypeScript
// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const {
handlers,
auth,
signIn,
signOut
} = NextAuth({
providers: [GitHub],
// Senior Tip: Always use the 'jwt' strategy for better scalability
// in serverless environments.
session: { strategy: "jwt" },
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
}
return true;
},
},
});
2. The Route Handler
Next, we need to expose the authentication API. Thanks to our setup above, the route handler is now just two lines of code.
TypeScript
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
3. Protecting Routes: Middleware vs. Server Components
As a senior dev, I recommend Middleware for global "gates" and Server Components for granular data protection.
The Middleware Gate
This prevents unauthenticated users from even hitting your server components for protected routes.
TypeScript
// middleware.ts
export { auth as middleware } from "@/auth";
export const config = {
// Protect everything except public assets and login
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
The Server Component Check
Inside a page, you can access the session without any "useEffect" or "loading" states. It’s instant and secure.
TypeScript
// app/dashboard/page.tsx
import { auth } from "@/auth";
export default async function Dashboard() {
const session = await auth();
if (!session) return <div>Please log in</div>;
return (
<main>
<h1>Welcome back, {session.user?.name}</h1>
{/* Personalized content rendered on the server */}
</main>
);
}
4. The Client Side: Interactivity
While we prefer server-side checks, you still need buttons. Use the exported signIn and signOut functions.
TypeScript
// components/AuthButtons.tsx
import { signIn, signOut } from "@/auth";
export function LoginButton() {
return (
<form action={async () => {
"use server";
await signIn("github");
}}>
<button type="submit">Sign in with GitHub</button>
</form>
);
}
Senior Engineering Checklist
- Secret Management: Ensure AUTH_SECRET is set in your .env. Without it, your production build will fail.
- Type Safety: If you're adding custom fields (like role) to the user object, remember to extend the Session interface in a next-auth.d.ts file.
- Edge Compatibility: Auth.js v5 is built to run on the Edge. Avoid importing heavy Node-only libraries in your main auth.ts if you plan to use Edge Runtime.
Ad Placeholder
Slot: MULTIPLEX_AD_SLOT_1