Better Auth integrates with Next.js through dedicated helpers for the App Router, Pages Router, server components, server actions, and middleware. Before you start, make sure you have a Better Auth instance configured. If you haven’t done that yet, check out the installation.
Create an API route
Create a route handler at app/api/auth/[...all]/route.ts and re-export the HTTP method handlers produced by toNextJsHandler.app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
toNextJsHandler also exports PATCH, PUT, and DELETE handlers. The destructured form above is the minimal configuration required for most use cases.
Create a catch-all API route at pages/api/auth/[...all].ts. Use toNodeHandler from better-auth/node and disable Next.js’s built-in body parser.pages/api/auth/[...all].ts
import { toNodeHandler } from "better-auth/node";
import { auth } from "@/lib/auth";
// Disallow body parsing — Better Auth handles it manually
export const config = { api: { bodyParser: false } };
export default toNodeHandler(auth.handler);
You can change the base path in your Better Auth configuration, but /api/auth is the recommended default.
Create a client
Create an auth client instance and export it from a shared module. Import from better-auth/react to get React-specific helpers such as the useSession hook.
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
// optional client configuration
});
The client uses nano-stores internally so reactive values re-render components automatically. Network requests use better-fetch, whose options can be forwarded through the client constructor.
useSession hook
"use client";
import { authClient } from "@/lib/auth-client";
export default function Dashboard() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <p>Loading...</p>;
if (!session) return <p>Not signed in</p>;
return <h1>Welcome, {session.user.name}</h1>;
}
Server-side session access
The auth.api object exposes every Better Auth endpoint as a callable function, including all plugin endpoints.
Server components (RSC)
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return <div>Not authenticated</div>;
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
</div>
);
}
RSCs cannot set cookies. The cookie cache will not be refreshed until the server is interacted with from the client via server actions or route handlers. See Session Management for details.
Server actions
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export const getUser = async () => {
"use server";
const session = await auth.api.getSession({
headers: await headers(),
});
return session;
};
Server action cookies
When you call functions that set cookies (such as signInEmail or signUpEmail) inside a server action, cookies are not set automatically because Next.js requires the cookies helper from next/headers.
Use the nextCookies plugin to handle this transparently:
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
// ...your config
plugins: [nextCookies()], // must be the last plugin in the array
});
With the plugin in place, cookie-setting calls work directly from server actions:
"use server";
import { auth } from "@/lib/auth";
export const signIn = async () => {
await auth.api.signInEmail({
body: {
email: "user@email.com",
password: "password",
},
});
};
Route protection
In Next.js middleware it is recommended to only check for the existence of a session cookie for redirects — making database calls in middleware adds latency to every request. Perform full session validation inside individual pages or route handlers instead.
Next.js 16+ (Proxy)
Next.js 16 replaces middleware with a “proxy”. The Node.js runtime is available by default, enabling full session validation.
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
export async function proxy(request: NextRequest) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard"],
};
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function proxy(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
if (!sessionCookie) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard"],
};
Migrating from middleware: Rename middleware.ts to proxy.ts and rename the exported function from middleware to proxy. All Better Auth methods work identically. You can also use the Next.js codemod: npx @next/codemod@canary middleware-to-proxy .
Next.js 15.2.0+ (Node.js runtime middleware)
From Next.js 15.2.0 you can opt into the Node.js runtime in middleware to make database calls directly.
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
runtime: "nodejs", // required for auth.api calls
matcher: ["/dashboard"],
};
Node.js runtime in middleware is experimental before Next.js 16. Consider upgrading to Next.js 16+ for stable proxy support.
Next.js 13–15.1.x (Edge runtime middleware)
Edge runtime middleware cannot make database calls. Use cookie-based checks for optimistic redirects, or fetch the session over HTTP.
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
if (!sessionCookie) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard"],
};
If you use a custom cookie name or prefix, pass the options:const sessionCookie = getSessionCookie(request, {
cookieName: "my_session_cookie",
cookiePrefix: "my_prefix",
});
Use better-fetch (or any fetch library) to call the session endpoint from middleware.import { betterFetch } from "@better-fetch/fetch";
import type { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
type Session = typeof auth.$Infer.Session;
export async function middleware(request: NextRequest) {
const { data: session } = await betterFetch<Session>(
"/api/auth/get-session",
{
baseURL: request.nextUrl.origin,
headers: {
cookie: request.headers.get("cookie") || "",
},
},
);
if (!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard"],
};
import { NextRequest, NextResponse } from "next/server";
import { getCookieCache } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const session = await getCookieCache(request);
if (!session) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
getSessionCookie only checks for the existence of a session cookie — it does not validate it. Anyone can manually create a cookie to bypass this check. Always validate the session on the server for any protected action or data access.
Per-page auth checks
For the strongest security, validate the session inside each protected page:
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/sign-in");
}
return <h1>Welcome, {session.user.name}</h1>;
}