Authorization in Next.js ensures that authenticated users only access what they’re allowed to. There are two main types of checks:
Use proxy.ts to redirect users based on session data:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function proxy(req: NextRequest) {
const path = req.nextUrl.pathname
const isProtected = protectedRoutes.includes(path)
const isPublic = publicRoutes.includes(path)
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (isProtected && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
if (isPublic && session?.userId && !path.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}Centralize your auth logic with a DAL. Start with verifySession():
// app/lib/dal.ts
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})Use it in data requests:
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
columns: { id: true, name: true, email: true },
})
return data[0]
})Return only the necessary fields to the client:
// app/lib/dto.ts
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer) {
return true
}
function canSeePhoneNumber(viewer, team) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug) {
const data = await db.query.users.findMany({ where: eq(users.slug, slug) })
const user = data[0]
const currentUser = await getUser(user.id)
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}Use verifySession() to conditionally render based on user role:
// app/dashboard/page.tsx
import { verifySession } from '@/app/lib/dal'
export default async function Dashboard() {
const session = await verifySession()
const userRole = session?.user?.role
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}Due to partial rendering, avoid doing auth checks directly in layouts. Instead, fetch user data and rely on DAL to enforce authorization:
// app/layout.tsx
export default async function Layout({ children }) {
const user = await getUser()
return (
// ...
)
}Authorization in Next.js combines fast optimistic checks with secure database validation. By using Proxy, DAL, DTOs, and role-based rendering, you can build scalable and secure access control across your application.