JSTAcademy
0 XP
Dashboard
Crash Courses
Middleware & Authentication
11 min
Masters+175 XP
Crash Courses · Masters

Middleware & Authentication

Protect routes and redirect users with middleware and Next.js auth patterns.
11 min read+175 XP on completionCert: Next.js Crash Course
Tap any word in the text below to start reading from there.

Middleware & Authentication

Middleware runs at the CDN edge before every matching request — before the request reaches your server, before the database is touched. This makes it ideal for:

  • Authentication guards (redirect to login if no session)
  • Geo-based redirects (different content by country)
  • A/B testing (cookie-based feature flags)
  • Rate limiting headers

Because middleware runs on the Edge Runtime (a V8 environment, not full Node.js), it has no access to fs, path, or other Node APIs. It runs in milliseconds near the user.

Basic Middleware — Auth Guard

// middleware.ts (root of the project, next to package.json)
import { NextRequest, NextResponse } from 'next/server'

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value
  const { pathname } = req.nextUrl

  const isProtected = pathname.startsWith('/dashboard') ||
                      pathname.startsWith('/courses') ||
                      pathname.startsWith('/profile')

  if (isProtected && !session) {
    const loginUrl = new URL('/login', req.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()  // proceed to the route normally
}

// Only run middleware on these routes (not static files, not API routes)
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|api/).*)',
  ],
}

The matcher pattern excludes Next.js internals and static assets — if you skip this, middleware runs on every image and CSS file too.

Role-Based Access

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value
  const role = req.cookies.get('role')?.value
  const { pathname } = req.nextUrl

  // Admin-only routes
  if (pathname.startsWith('/admin') && role !== 'admin') {
    return NextResponse.redirect(new URL('/403', req.url))
  }

  // Any authenticated route
  if (
    (pathname.startsWith('/dashboard') || pathname.startsWith('/courses')) &&
    !session
  ) {
    const loginUrl = new URL('/login', req.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

Reading Auth in Server Components

For auth checks in server components (not middleware), use the cookies() helper:

import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { verifyToken } from '@/lib/auth'

async function getUser() {
  const cookieStore = await cookies()
  const token = cookieStore.get('auth-token')?.value

  if (!token) redirect('/login')  // throws internally — stops rendering

  const user = await verifyToken(token)
  if (!user) redirect('/login')

  return user
}

// Protected page — automatically redirects if not authenticated
export default async function DashboardPage() {
  const user = await getUser()  // redirects to /login if no valid session

  return <Dashboard user={user} />
}

Adding User Context to Headers

Middleware can attach data to headers, making it available to server components downstream:

export function middleware(req: NextRequest) {
  const token = req.cookies.get('auth-token')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }

  // Decode token (lightweight — no DB call at edge)
  const userId = decodeJwtSub(token)  // edge-compatible JWT decode

  // Pass user id to downstream server components via headers
  const res = NextResponse.next()
  res.headers.set('x-user-id', userId)
  return res
}
0%