Next.js Middleware — Authentication and Routing

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js Middleware — Authentication and Routing

Middleware in Next.js runs on the edge before your routes execute, enabling authentication checks and request modification.

Creating Middleware

Create a middleware.ts file in your project root:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  console.log('Middleware running for:', request.nextUrl.pathname)

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*']
}

The matcher option controls which routes trigger middleware.

Authentication Middleware

Protect routes by checking authentication:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET!)

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

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

  try {
    await jwtVerify(token, secret)
    return NextResponse.next()
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*']
}

Redirects Based on Path

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Redirect /old-page to /new-page
  if (pathname === '/old-page') {
    return NextResponse.redirect(new URL('/new-page', request.url))
  }

  // Redirect based on locale
  if (pathname === '/blog') {
    const locale = request.headers.get('accept-language')?.split('-')[0] || 'en'
    return NextResponse.redirect(new URL(`/${locale}/blog`, request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}

Request/Response Modification

Add headers or modify requests:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next()

  // Add custom headers
  response.headers.set('X-Custom-Header', 'value')
  response.headers.set('X-Request-ID', crypto.randomUUID())

  // Add user info to request
  const userId = request.cookies.get('user-id')?.value
  if (userId) {
    response.headers.set('X-User-ID', userId)
  }

  return response
}

API Rate Limiting Middleware

Implement rate limiting at the edge:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const rateLimitStore = new Map()

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api')) {
    const clientIp = request.headers.get('x-forwarded-for') || 'unknown'
    const now = Date.now()
    const windowStart = now - 60000 // 1 minute window

    const requests = rateLimitStore.get(clientIp) || []
    const recentRequests = requests.filter(time => time > windowStart)

    if (recentRequests.length >= 100) {
      return new NextResponse(
        JSON.stringify({ error: 'Rate limit exceeded' }),
        { status: 429 }
      )
    }

    recentRequests.push(now)
    rateLimitStore.set(clientIp, recentRequests)
  }

  return NextResponse.next()
}

Locale-Based Routing

Handle multiple languages:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const locales = ['en', 'es', 'fr', 'de']

function getLocale(request: NextRequest): string {
  const localeFromPath = locales.find(
    locale => request.nextUrl.pathname.startsWith(`/${locale}`)
  )

  if (localeFromPath) return localeFromPath

  const localeFromCookie = request.cookies.get('locale')?.value
  if (localeFromCookie) return localeFromCookie

  const localeFromHeader = request.headers
    .get('accept-language')
    ?.split(',')[0]
    .split('-')[0]

  return localeFromHeader && locales.includes(localeFromHeader)
    ? localeFromHeader
    : 'en'
}

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Skip public files
  if (pathname.match(/^\/?(?:_next|api|public|favicon)/)) {
    return NextResponse.next()
  }

  const hasLocale = locales.some(locale => pathname.startsWith(`/${locale}`))

  if (!hasLocale) {
    const locale = getLocale(request)
    return NextResponse.redirect(
      new URL(`/${locale}${pathname}`, request.url)
    )
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next|api|public|favicon).*)']
}

Feature Flags Middleware

Control feature access via middleware:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const enabledFeatures = {
  darkMode: true,
  betaFeatures: false,
  analytics: true
}

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/beta')) {
    if (!enabledFeatures.betaFeatures) {
      return NextResponse.redirect(new URL('/404', request.url))
    }
  }

  const response = NextResponse.next()

  // Pass feature flags to app
  Object.entries(enabledFeatures).forEach(([key, value]) => {
    response.headers.set(`X-Feature-${key}`, String(value))
  })

  return response
}

Authentication with Session

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Unprotected routes
  if (['/login', '/register', '/'].includes(pathname)) {
    return NextResponse.next()
  }

  // Check session for protected routes
  const session = await getSession(request)

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

  // Admin routes require admin role
  if (pathname.startsWith('/admin') && session.role !== 'admin') {
    return NextResponse.redirect(new URL('/denied', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)'
  ]
}

Real-World Example: Multi-Tenant Middleware

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host')!
  const pathname = request.nextUrl.pathname

  // Extract subdomain
  const subdomain = hostname.split('.')[0]

  // Handle root domain
  if (subdomain === 'localhost' || subdomain === 'example') {
    return NextResponse.next()
  }

  // Route tenant requests to /tenant/[subdomain] without changing URL
  const tenantPath = `/tenant/${subdomain}${pathname}`

  return NextResponse.rewrite(
    new URL(tenantPath, request.url)
  )
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image).*)']
}

FAQ

Q: How does middleware differ from API routes? A: Middleware runs before all routes and has access to requests at the edge. API routes are specific endpoints. Middleware is ideal for cross-cutting concerns.

Q: Can middleware access the database? A: Yes, but edge middleware has limitations. Database calls work but may have latency. Keep middleware logic lightweight.

Q: What's the difference between matcher array and condition? A: matcher uses glob patterns to specify paths. You can also use conditional checks inside the middleware function for more complex logic.


Middleware enables powerful request-level control and security patterns in Next.js.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro