[Next.js Tutorial] Middleware: A Practical Guide with Real-World Examples

In the modern web development world with Next.js, handling requests before they reach a page or API is essential. This is where Middleware shines, acting as a smart and flexible "gatekeeper" that lets you control, modify, and direct data flow efficiently.

Middleware in Nextjs

This article will help you explore Middleware in Next.js in detail from A-Z: what it is, why it matters, and how to harness its full power.

1. What is Middleware in Next.js? 💡

Imagine your Next.js app as a luxury building. Before a guest (request) can enter a specific room (page or API), they must pass a gatekeeper (Middleware) in the main hall. This gatekeeper can:

  • Check credentials: Authenticate whether the guest is allowed in (Authentication).
  • Direct traffic: Guide the guest to a more suitable room (Redirects/Rewrites).
  • Log information: Record entry and exit times (Logging).
  • Add extra info: Attach special "badges" to the guest's card before they enter (Modifying headers).

Technically, Middleware in Next.js is code that runs on the server (specifically, on the Edge Runtime) before a request is completed. It lets you run code before a page or API route is rendered. This way, you can change the response based on the user's request.

Defining Middleware is simple: just create a middleware.ts (or .js) file in your project's root directory.

2. Why is Middleware a "Game Changer"? 🤔

Middleware has changed the game in Next.js architecture for several reasons:

  • Superior speed and performance: Middleware runs on Vercel's Edge Runtime—a super lightweight JavaScript environment deployed globally (Edge Network). This means your code executes geographically close to users, minimizing latency. It's much faster than spinning up a full Node.js environment for simple logic.
  • Centralized logic: Instead of duplicating authentication checks across many pages, you can consolidate them in a single Middleware file. This keeps your code clean, maintainable, and scalable.
  • Smooth user experience: You can personalize content or redirect users instantly, without waiting for client-side JavaScript. For example, redirecting users based on their location immediately.
  • Enhanced security: Middleware is your first line of defense. It can block suspicious requests, check authentication tokens, or restrict access to sensitive resources before they reach your main app logic.

3. Middleware Features: Real-World Use Cases 🚀

So, what can you actually do with Middleware? Here are the most common and powerful use cases.

a. Authentication

This is the classic use case. You can check if a user is logged in by inspecting cookies or tokens in the header. If not, redirect them to the login page.

Example: Simple authentication check.

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

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

  // If no token and user tries to access dashboard
  if (!authToken && request.nextUrl.pathname.startsWith('/dashboard')) {
    const loginUrl = new URL('/login', request.url)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

// Apply middleware only to specific routes
export const config = {
  matcher: ['/dashboard/:path*'],
}

b. A/B Testing and Feature Flags

Want to test a new feature with a small group of users? Middleware is perfect. Based on cookies or other parameters, you can rewrite the URL to show users version A or B of a page without changing the browser's URL.

Example: Feature experiment.

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

export function middleware(request: NextRequest) {
  const bucket = request.cookies.get('bucket')?.value

  if (bucket === 'new-feature') {
    // Rewrite URL to show new page but keep original URL
    return NextResponse.rewrite(new URL('/new-home', request.url))
  }

  return NextResponse.next()
}

c. Internationalization (i18n)

Middleware can automatically detect the user's preferred language from the Accept-Language header or their location (request.geo) and redirect them to the appropriate language version (e.g., /en/about or /vi/about).

Example: Auto-detect user language.

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

const locales = ['en', 'vi', 'jp']

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
  )

  if (pathnameHasLocale) return

  // Default to Vietnamese
  const locale = 'vi'
  request.nextUrl.pathname = `/${locale}${pathname}`
  return NextResponse.redirect(request.nextUrl)
}

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

d. Logging & Analytics

Before a request is processed, you can log important info like the accessed path, time, or user location for analytics and error tracking.

4. Middleware Configuration and Optimization (Matcher)

You don't always want Middleware to run on every request. That would impact performance. Next.js provides a config object with a matcher property so you can specify exactly which paths trigger Middleware.

// middleware.ts
export const config = {
  // Can use a string, array of strings, or regex
  matcher: [
    /*
     * Match all paths except those starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

Using matcher correctly is crucial to ensure Middleware only runs when needed, keeping your app fast and efficient.

5. Middleware Limitations

Although powerful, Middleware on the Edge Runtime has some limitations:

  • No full Node.js API support: Since it doesn't run in a traditional Node.js environment, some Node.js APIs (like direct file system access with fs) are unavailable.
  • Execution time: There are execution time limits to ensure performance. Your logic needs to be lightweight and fast.
  • Bundle size: Your Middleware code has a size limit.

However, for most use cases like authentication, routing, or A/B testing, these limitations are not an issue.

Conclusion: Master Middleware, Master Next.js

Middleware is not just a feature; it's a shift in Next.js application architecture. It brings edge logic power, enabling faster, safer, and more personalized user experiences.

By understanding and applying Middleware smartly, you have one of the most powerful tools to elevate your Next.js projects. Start integrating this versatile "gatekeeper" into your app today!

Related Posts

[Next.js Tutorial] Server Actions & Mutations: A Practical Guide with Real Examples

Learn about Server Actions and Mutations in Next.js to optimize data handling. This article provides a detailed guide from basics to advanced usage, helping you build faster and more efficient apps.

[Next.js Tutorial] Dynamic Routes: How to Use and Optimize

Learn how to create and manage Dynamic Routes in Next.js (App Router). This article provides a step-by-step guide to help you build dynamic, SEO-friendly web pages efficiently.

[Next.js Tutorial] Boost Your Website Traffic: The Complete Guide to Metadata and SEO

Next.js is a powerful framework for SEO, but do you know how to use Metadata correctly? Learn on-page optimization techniques to improve SEO performance and achieve top Google rankings.

[Next.js Tutorial] Layouts: A Detailed Guide to Optimizing Your Website UI

Want to manage your Next.js website layout flexibly and efficiently? This article will guide you on using layouts to speed up development and improve user experience.