Next.js Middleware: Vodič za autentifikaciju, sigurnost i napredne obrasce

Praktični vodič za Next.js middleware — od JWT autentifikacije i rate limitinga do internacionalizacije. Saznajte zašto CVE-2025-29927 dokazuje da middleware nikad ne smije biti jedini sloj zaštite.

Uvod: Zašto je middleware ključan za moderne Next.js aplikacije

Ako ste ikada izgradili full-stack aplikaciju u Next.js-u, znate koliko je važno imati centralizirano mjesto za obradu zahtjeva prije nego što oni dosegnu vaše stranice ili API rute. Upravo tu na scenu stupa middleware — moćan mehanizam koji vam omogućuje da presretnete, modificirate i kontrolirate svaki HTTP zahtjev koji prolazi kroz vašu aplikaciju. Bez obzira radite li na jednostavnom blogu ili složenoj SaaS platformi, middleware će prije ili kasnije postati neizostavni dio vaše arhitekture.

Middleware u Next.js-u djeluje kao "čuvar na vratima" vaše aplikacije. Zamislite ga kao sloj koji sjedi između korisnikovog preglednika i vaših stranica — svaki zahtjev prolazi kroz njega prije nego što stigne do odredišta. To ga čini idealnim mjestom za implementaciju autentifikacije, preusmjeravanja, lokalizacije, ograničavanja brzine zahtjeva i mnogih drugih obrazaca koji zahtijevaju obradu na razini zahtjeva.

Umjesto da raspršite sigurnosne provjere po desetcima datoteka, middleware vam omogućuje da centralizirate tu logiku na jednom mjestu.

U ekosustavu App Routera, middleware je dobio posebno značenje. Za razliku od tradicionalnih pristupa gdje biste logiku autentifikacije raspršili po pojedinačnim rutama ili koristili Higher-Order Componente (HOC) za zaštitu stranica, middleware vam omogućuje da to učinite na jednom mjestu, čisto i efikasno. Ovo je posebno korisno u velikim timovima gdje konzistentnost sigurnosnih provjera može biti izazov. No, kao što ćemo vidjeti u ovom vodiču, middleware nije srebrni metak — ima svoja ograničenja, poznate ranjivosti i zamke koje morate razumjeti da biste ga koristili ispravno.

U ovom vodiču ćemo proći kroz sve aspekte Next.js middlewarea: od osnovne konfiguracije i razumijevanja životnog ciklusa zahtjeva, preko naprednih obrazaca autentifikacije i sigurnosti, do internacionalizacije i A/B testiranja. Posebnu pozornost ćemo posvetiti kritičnoj CVE-2025-29927 ranjivosti koja je potresla Next.js zajednicu i pokazala zašto se nikada ne smijete osloniti isključivo na middleware za sigurnost. Također ćemo razmotriti rate limiting s Upstash Redisom, geolokacijske obrasce i najbolje prakse za organizaciju složenog middlewarea u produkcijskim aplikacijama.

Kako middleware funkcionira u Next.js-u

Middleware u Next.js-u definira se kroz jednu posebnu datoteku — middleware.ts (ili middleware.js) — koja se mora nalaziti u korijenu vašeg projekta, na istoj razini kao direktorij app ili src. Ovo je jedina datoteka koju Next.js prepoznaje kao middleware, i ona se automatski primjenjuje na sve rute vaše aplikacije (osim ako ne konfigurirate drugačije putem matchera).

Važno je napomenuti da za razliku od Express.js-a ili sličnih frameworka, Next.js podržava samo jednu middleware datoteku — ne možete imati više middleware funkcija koje se ulančavaju jedna za drugom na razini frameworka, iako možete simulirati taj obrazac unutar same middleware funkcije.

Osnovna struktura middleware datoteke

Middleware datoteka izvozi jednu funkciju koja prima NextRequest objekt i vraća NextResponse. Funkcija može biti sinkrona ili asinkrona, ovisno o tome trebate li čekati na neku eksternu operaciju poput verifikacije tokena ili dohvaćanja podataka iz baze. Evo najjednostavnijeg primjera:

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

export function middleware(request: NextRequest) {
  // Log every request
  console.log(`[Middleware] ${request.method} ${request.nextUrl.pathname}`);

  // Continue to the next handler
  return NextResponse.next();
}

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

Ova funkcija se izvršava za svaki zahtjev koji odgovara konfiguriranom matcheru. Objekt NextRequest proširuje standardni Web API Request objekt s dodatnim korisnim svojstvima specifičnim za Next.js, kao što su nextUrl (koji uključuje parsirane informacije o URL-u uključujući pathname, search parametre i hash), kolačići (kroz praktičan cookies API), te geolokacijski podaci (dostupni na platformama poput Vercela).

Objekt NextResponse pak proširuje standardni Response s metodama za preusmjeravanje, prepisivanje URL-ova i manipulaciju kolačića.

Životni ciklus zahtjeva

Važno je razumjeti gdje se middleware nalazi u životnom ciklusu zahtjeva, jer to izravno utječe na to koje operacije u njemu imaju smisla, a koje ne. Evo detaljnog pregleda koraka kroz koje zahtjev prolazi:

  1. Korisnik šalje HTTP zahtjev — preglednik traži stranicu ili API resurs. Zahtjev dolazi na vaš poslužitelj (ili edge čvor, ako koristite platformu s edge deploymentom).
  2. Middleware presreće zahtjev — prije nego što zahtjev dođe do bilo koje rute, middleware ga obrađuje. Ovo se događa čak i prije nego što Next.js počne renderirati stranicu ili izvršavati Server Componente.
  3. Middleware donosi odluku — može nastaviti zahtjev (NextResponse.next()), preusmjeriti korisnika (NextResponse.redirect()), prepisati URL (NextResponse.rewrite()) ili vratiti odgovor izravno. Ovo je ključni trenutak u kojem se odlučuje sudbina zahtjeva.
  4. Zahtjev dolazi do rute — ako middleware dopusti, zahtjev nastavlja do odgovarajuće stranice, layouta ili API rute. Od ovog trenutka, standardni Next.js životni ciklus preuzima kontrolu.

Ono što je posebno važno razumjeti jest da middleware ima pristup zahtjevu, ali ne i potpunom kontekstu renderiranja. Ne možete pristupiti bazi podataka izravno (osim putem HTTP poziva), ne možete koristiti React kontekst, i ne možete direktno mijenjati tijelo odgovora koje generira stranica.

Middleware je zapravo "prolazna stanica" koja odlučuje kamo zahtjev ide, a ne "procesor" koji generira konačni odgovor.

Edge Runtime vs Node.js Runtime

Jedna od najvažnijih tehničkih pojedinosti o Next.js middlewareu je da se on izvršava u Edge Runtime okruženju, a ne u standardnom Node.js runtimeu. Ovo je dizajnerska odluka koja ima duboke implikacije na to kako pišete middleware i koje biblioteke možete koristiti. Edge Runtime je zapravo V8 izolat — lako virtualizirano okruženje koje se pokreće na edge čvorovima blizu korisnika, pružajući minimalne latencije.

Evo što to praktično znači:

  • Ograničen API — ne možete koristiti sve Node.js module. Na primjer, fs modul nije dostupan, kao ni mnogi drugi Node.js specifični API-ji poput child_process, net, ili dgram. Također nemaste pristup globalnim varijablama specifičnim za Node.js poput __dirname ili process.cwd().
  • Brza izvedba — Edge Runtime je optimiziran za niske latencije jer se izvršava bliže korisniku na CDN čvorovima. Hladni startovi su znatno brži nego u punom Node.js okruženju jer se V8 izolati pokrevću u milisekundama, a ne sekundama.
  • Ograničena veličina — postoji ograničenje na veličinu middleware koda (obično 1 MB nakon kompresije). Ovo znači da morate biti selektivni u tome koje ovisnosti uvozite.
  • Web API-ji — imate pristup standardnim Web API-jima kao što su fetch, crypto, TextEncoder, TextDecoder, URL, URLSearchParams, Headers, Request, Response i drugi. Ovi API-ji su isti kao u pregledniku, što čini kod prenosivim.
  • Bez dugotrajnih konekcija — ne možete održavati WebSocket konekcije ili persistentne veze s bazom podataka. Svako izvršavanje middlewarea je kratkotrajno i bezstanjsko (stateless).

Ovo znači da morate biti pažljivi s bibliotekama koje uvozite u middleware — mnoge Node.js biblioteke jednostavno neće raditi. Na primjer, ne možete koristiti bcrypt (koji ovisi o nativnim C++ modulima), ali možete koristiti jose biblioteku za JWT operacije jer je napisana u čistom JavaScriptu kompatibilnom s Web API-jima. Također ne možete koristiti ORM-ove poput Prisma koji zahtijevaju nativne binarne datoteke, ali možete koristiti HTTP-bazirane klijente poput Upstash Redisa koji komuniciraju putem REST API-ja.

Konfiguracija matchera

Matcher je mehanizam koji vam omogućuje da precizno odredite na koje rute se vaš middleware primjenjuje. Bez matchera, middleware bi se izvršavao za apsolutno svaki zahtjev, uključujući zahtjeve za statičkim resursima (JavaScript datoteke, CSS, slike, fontovi) i druge datoteke koje obično ne trebaju obradu middlewarea.

Pravilna konfiguracija matchera je ključna za performanse vaše aplikacije jer nepotrebno izvršavanje middlewarea za statičke resurse može značajno usporiti učitavanje stranice.

Osnovni obrasci matchera

Matcher podržava regularne izraze i jednostavne putanje s dinamičkim segmentima. Next.js koristi vlastiti format za definiranje putanja koji je inspiriran Express.js stilom ruta, ali s nekim razlikama. Evo nekoliko čestih konfiguracija koje pokrivaju većinu scenarija:

// Primjena na specifične rute
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/api/:path*'],
};

// Primjena na sve rute osim statičkih resursa
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
};

// Primjena samo na API rute
export const config = {
  matcher: ['/api/:path*'],
};

// Kombinacija specifičnih ruta i obrazaca
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/account/:path*',
    '/api/((?!public).*)',  // API rute osim /api/public/*
  ],
};

Sintaksa :path* označava dinamički segment koji odgovara nula ili više segmenata putanje. Na primjer, /dashboard/:path* će odgovarati rutama /dashboard, /dashboard/settings, /dashboard/users/123 i svim drugim podrutama. Negativni lookahead obrasci poput (?!_next/static) koriste se za isključivanje specifičnih putanja iz matchera.

Napredni regex obrasci

Za složenije scenarije, možete koristiti napredne regularne izraze unutar matchera. Ovo je posebno korisno kada trebate isključiti više tipova datoteka ili odgovarati na specifične obrasce putanja:

export const config = {
  matcher: [
    // Match all paths except static files and images
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Programatsko filtriranje unutar middlewarea

Ponekad je lakše filtrirati rute unutar same middleware funkcije umjesto korištenja složenih regex obrazaca u matcheru. Ovaj pristup je posebno koristan kada imate složenu logiku koja ovisi o više faktora osim same putanje — na primjer, kada različite rute zahtijevaju različite razine autentifikacije ili kada trebate dinamički mijenjati pravila pristupa:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const protectedRoutes = ['/dashboard', '/account', '/settings'];
const publicRoutes = ['/login', '/register', '/forgot-password'];

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

  // Check if the current route is protected
  const isProtected = protectedRoutes.some(route =>
    pathname.startsWith(route)
  );

  // Check if the current route is public
  const isPublic = publicRoutes.some(route =>
    pathname.startsWith(route)
  );

  if (isProtected) {
    // Handle authentication logic
    const token = request.cookies.get('session-token')?.value;
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  if (isPublic) {
    // Redirect authenticated users away from public pages
    const token = request.cookies.get('session-token')?.value;
    if (token) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return NextResponse.next();
}

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

Ovaj pristup je čitljiviji i lakši za održavanje, posebno kada imate mnogo ruta s različitim pravilima pristupa. Također omogućuje dinamičku konfiguraciju — na primjer, možete učitati listu zaštićenih ruta iz konfiguracijske datoteke ili čak iz vanjskog servisa.

Obrasci autentifikacije u middlewareu

Autentifikacija je najčešći i najvažniji razlog zašto programeri posežu za middlewareom. Umjesto da provjeravate sesiju na svakoj stranici pojedinačno — što je podložno pogreškama i teško za održavanje — middleware vam omogućuje centraliziranu provjeru koja se konzistentno primjenjuje na sve zaštićene rute.

Ovo je posebno vrijedna praksa u timovima gdje više programera radi na istom projektu, jer smanjuje rizik da netko slučajno zaboravi dodati provjeru autentifikacije na novu stranicu ili API rutu.

Međutim, kao što ćemo vidjeti u odjeljku o CVE-2025-29927, middleware autentifikacija mora biti dodatni sloj zaštite, ne jedini. Unatoč tome, ispravno implementirana middleware autentifikacija značajno poboljšava sigurnosni profil vaše aplikacije i pruža bolji korisnički doživljaj jer neautorizirani korisnici bivaju preusmjereni prije nego što se stranica uopće počne renderirati.

JWT validacija u middlewareu

JWT (JSON Web Token) je jedan od najpopularnijih mehanizama za autentifikaciju u modernim web aplikacijama. Njegova prednost u kontekstu middlewarea je što je verifikacija potpisa brza operacija koja ne zahtijeva poziv prema bazi podataka — sve informacije potrebne za verifikaciju nalaze se u samom tokenu i tajnom ključu.

Evo kako možete implementirati JWT validaciju u middlewareu koristeći jose biblioteku (koja je kompatibilna s Edge Runtimeom):

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

const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET || 'your-secret-key'
);

async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET, {
      algorithms: ['HS256'],
    });
    return payload;
  } catch (error) {
    return null;
  }
}

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

  // Skip authentication for public routes
  if (pathname.startsWith('/login') || pathname.startsWith('/register')) {
    return NextResponse.next();
  }

  // Get token from cookie or Authorization header
  const token =
    request.cookies.get('auth-token')?.value ||
    request.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    if (pathname.startsWith('/api/')) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      );
    }
    return NextResponse.redirect(new URL('/login', request.url));
  }

  const payload = await verifyToken(token);

  if (!payload) {
    // Token is invalid or expired
    const response = pathname.startsWith('/api/')
      ? NextResponse.json({ error: 'Invalid token' }, { status: 401 })
      : NextResponse.redirect(new URL('/login', request.url));

    // Clear the invalid cookie
    response.cookies.delete('auth-token');
    return response;
  }

  // Add user info to request headers for downstream use
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-id', payload.sub as string);
  requestHeaders.set('x-user-role', payload.role as string);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*', '/api/((?!auth).*)'],
};

Primijetite kako se u ovom primjeru razlikuje rukovanje API rutama i stranama — API rute vraćaju JSON odgovor s odgovarajućim HTTP statusom (401), dok stranice preusmjeravaju korisnika na stranicu za prijavu. Ovo je važan obrazac jer API klijenti (mobilne aplikacije, druge usluge) očekuju strukturirane JSON odgovore, dok preglednici očekuju preusmjeravanja.

Također, dodavanje korisničkih podataka u zaglavlja zahtjeva omogućuje downstream komponentama pristup tim podacima bez ponovnog dekodiranja tokena.

Provjera sesije s kolačićima

Za aplikacije koje koriste sesijsku autentifikaciju temeljenu na kolačićima (server-side sessions), pristup je nešto drugačiji. Umjesto lokalne verifikacije tokena, trebate kontaktirati vaš session store kako biste potvrdili da je sesija valjana. Ovo dodaje mrežni poziv u middleware, ali pruža veću kontrolu jer možete odmah poništiti sesije na serveru:

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

export async function middleware(request: NextRequest) {
  const sessionToken = request.cookies.get('session-id')?.value;

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

  // Validate session against your session store
  try {
    const response = await fetch(
      `${process.env.API_URL}/api/sessions/validate`,
      {
        headers: {
          'Cookie': `session-id=${sessionToken}`,
        },
      }
    );

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

    const session = await response.json();

    // Pass session data downstream via headers
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('x-session-user', JSON.stringify(session.user));

    return NextResponse.next({
      request: { headers: requestHeaders },
    });
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

Integracija s Auth.js (NextAuth)

Auth.js (ranije poznat kao NextAuth.js) nudi ugrađenu podršku za middleware koja značajno pojednostavljuje implementaciju autentifikacije. Ova integracija automatski dešifrira sesijski kolačić, verificira ga i stavlja podatke o sesiji na raspolaganje vašem middleware kodu.

Iskreno, ovo je daleko najjednostavniji pristup ako već koristite Auth.js za autentifikaciju u vašoj aplikaciji:

// middleware.ts
import { auth } from '@/auth';

export default auth((req) => {
  const { pathname } = req.nextUrl;
  const isLoggedIn = !!req.auth;

  // Define route groups
  const isProtectedRoute = pathname.startsWith('/dashboard') ||
                           pathname.startsWith('/settings');
  const isAuthRoute = pathname.startsWith('/login') ||
                      pathname.startsWith('/register');

  // Redirect unauthenticated users from protected routes
  if (isProtectedRoute && !isLoggedIn) {
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return Response.redirect(loginUrl);
  }

  // Redirect authenticated users from auth routes
  if (isAuthRoute && isLoggedIn) {
    return Response.redirect(new URL('/dashboard', req.url));
  }

  // Role-based access control
  if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
    return Response.redirect(new URL('/unauthorized', req.url));
  }
});

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

Auth.js pristup je elegantan jer automatski upravlja sesijama, osvježavanjem tokena i drugim složenim aspektima autentifikacije. Primijetite kako callbackUrl parametar sprema izvornu putanju korisnika — nakon uspješne prijave, korisnik će biti vraćen na stranicu kojoj je prvobitno pokušao pristupiti. Ovo je mali ali važan detalj korisničkog iskustva koji se često zaboravlja.

No, važno je zapamtiti da ni ovo ne zamjenjuje provjeru autorizacije na razini servera — o čemu ćemo detaljno govoriti u sljedećem odjeljku.

CVE-2025-29927: Kritična ranjivost middlewarea

U ožujku 2025. godine, otkrivena je kritična sigurnosna ranjivost u Next.js middlewareu, označena kao CVE-2025-29927, s CVSS ocjenom 9.1 od 10. Ova ranjivost je dramatično pokazala zašto se nikada ne smijete osloniti isključivo na middleware za sigurnost vaše aplikacije.

Ranjivost je pogodila stotine tisuća Next.js aplikacija diljem svijeta i izazvala val hitnih ažuriranja u cijeloj industriji.

Što je bio problem?

Next.js interno koristi HTTP zaglavlje x-middleware-subrequest za upravljanje internim preusmjeravanjima i sprječavanje beskonačnih petlji. Kada middleware izvrši podzahtjev (na primjer, prilikom prepisivanja URL-a ili internog preusmjeravanja), Next.js dodaje ovo zaglavlje da bi znao da je zahtjev već prošao kroz middleware i da ga ne treba ponovno obrađivati. Ovo je potpuno razuman interno mehanizam — problem je bio u njegovoj implementaciji.

Problem je bio u tome što vanjski napadači mogli su jednostavno dodati ovo zaglavlje u svoje zahtjeve, čime su kompletno zaobilazili middleware. Next.js nije verificirao izvor zaglavlja — nije provjeravao je li zahtjev doista interni podzahtjev ili vanjski zahtjev s lažnim zaglavljem. To znači da su sve provjere autentifikacije, autorizacije, rate limitinga i svih drugih sigurnosnih mehanizama implementiranih u middlewareu mogle biti zaobiđene jednim HTTP zaglavljem.

Napadač nije trebao znati nikakve lozinke, tokene ili tajne ključeve — samo je trebao dodati jedno zaglavlje u svoj zahtjev.

Primjer napada

Napad je bio zastrašujuće jednostavan — toliko jednostavan da je bio pristupačan čak i napadačima s minimalnim tehničkim znanjem. Napadač je trebao samo poslati zahtjev s ispravnim zaglavljem:

# Za Next.js verzije < 12.3.5
curl -H "x-middleware-subrequest: middleware" https://target-app.com/dashboard

# Za novije verzije, format zaglavlja je varirao
# ali princip je bio isti - middleware se potpuno preskakao

# Primjer s ponavljajućim prefiksom za verzije 13.x - 14.x
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" \
  https://target-app.com/admin

Ovaj jednostavan zahtjev potpuno bi zaobišao middleware, omogućujući napadaču pristup zaštićenim rutama bez ikakve autentifikacije. Ako je vaša aplikacija koristila middleware kao jedini sloj autentifikacije — što je, nažalost, bio čest obrazac u Next.js zajednici — napadač je mogao pristupiti svim zaštićenim stranicama, API rutama i administratorskim panelima.

Posebno zabrinjavajuće je bilo to što su mnogi tutoriali i službena dokumentacija promicali middleware kao primarno mjesto za autentifikaciju bez dovoljnog naglaska na potrebi za dodatnim slojevima zaštite.

Pogođene verzije

Ranjivost je utjecala na širok raspon Next.js verzija, što znači da je velika većina produkcijskih Next.js aplikacija bila potencijalno ranjiva:

  • Next.js 11.1.4 do 13.5.6 — sve verzije prije 13.5.9
  • Next.js 14.x — sve verzije prije 14.2.25
  • Next.js 15.x — sve verzije prije 15.2.3

Popravak je uključen u verzijama 12.3.5, 13.5.9, 14.2.25 i 15.2.3. Ako koristite bilo koju stariju verziju, hitno ažurirajte.

Vercel je brzo reagirao i objavio popravke za sve aktivno podržane verzije, ali mnoge self-hosted aplikacije ostale su ranjive tjednima jer njihovi timovi nisu bili svjesni problema.

Kako se zaštititi

Osim ažuriranja na popravljene verzije (što je apsolutni minimum), preporučuju se sljedeće dodatne mjere zaštite. Prvo, možete blokirati opasno zaglavlje na razini infrastrukture:

// next.config.js - Block the dangerous header at the infrastructure level
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'x-middleware-subrequest',
            value: '',  // Strip the header
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Na razini infrastrukture, ako koristite reverzni proxy kao što je Nginx ili Cloudflare, možete blokirati ovo zaglavlje prije nego što uopće stigne do vaše aplikacije:

# Nginx konfiguracija
proxy_set_header x-middleware-subrequest "";

Pristup "obrane u dubinu" (Defense in Depth)

CVE-2025-29927 nas uči najvažniju lekciju o sigurnosti web aplikacija: nikada se ne oslanjajte na jedan sloj zaštite. Ovaj princip, poznat kao "obrana u dubinu" (defense in depth), dolazi iz vojnog konteksta ali je potpuno primjenjiv na softversku sigurnost.

Ideja je jednostavna — ako jedan sloj zaštite zakaže, sljedeći ga podupire. Middleware bi trebao biti samo prvi sloj u višeslojnom sigurnosnom modelu:

// SLOJ 1: Middleware - prva linija obrane (ali NE jedina!)
// middleware.ts
export async function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

// SLOJ 2: Server Component - provjera na razini stranice
// app/dashboard/page.tsx
import { getServerSession } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await getServerSession();

  // UVIJEK provjerite sesiju i na razini stranice!
  if (!session) {
    redirect('/login');
  }

  // UVIJEK provjerite autorizaciju za specifične podatke!
  const userData = await getUserData(session.user.id);

  return <Dashboard data={userData} />;
}

// SLOJ 3: API ruta - provjera na razini API-ja
// app/api/user/data/route.ts
import { getServerSession } from '@/lib/auth';

export async function GET() {
  const session = await getServerSession();

  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Provjera na razini baze podataka
  const data = await db.query({
    where: { userId: session.user.id }, // Nikada ne vjerujte klijentskim podacima
  });

  return Response.json(data);
}

// SLOJ 4: Data Access Layer - posljednja linija obrane
// lib/dal.ts
import { getServerSession } from '@/lib/auth';

export async function getUserData(requestedUserId: string) {
  const session = await getServerSession();

  if (!session) {
    throw new Error('Authentication required');
  }

  // Provjerite ima li korisnik pravo pristupa traženim podacima
  if (session.user.id !== requestedUserId && session.user.role !== 'admin') {
    throw new Error('Access denied');
  }

  return db.users.findUnique({ where: { id: requestedUserId } });
}

Ovaj višeslojni pristup osigurava da čak i ako jedan sloj zaštite zakaže (kao što se dogodilo s middlewareom), ostali slojevi i dalje štite vašu aplikaciju. Data Access Layer (DAL) je posebno važan jer predstavlja posljednju liniju obrane — čak i ako napadač nekako zaobiđe sve prethodne slojeve, DAL osigurava da se podaci filtriraju prema korisnikovim pravima pristupa direktno na razini upita prema bazi podataka.

Ograničavanje brzine zahtjeva (Rate Limiting)

Ograničavanje brzine zahtjeva (rate limiting) je kritičan sigurnosni mehanizam koji sprječava zloupotrebu vaših API-ja i stranica. Bez rate limitinga, vaša aplikacija je ranjiva na brute force napade na autentifikaciju, DDoS napade, prekomjerno korištenje API-ja od strane pojedinih korisnika i automatsko scraping sadržaja.

Middleware je izvrsno mjesto za implementaciju rate limitinga jer presreće zahtjeve prije nego što dosegnu vaše rute, čime se smanjuje opterećenje na poslužitelju i štite se svi resursi jednako.

Implementacija s @upstash/ratelimit

Jedna od najpopularnijih biblioteka za rate limiting na Edge Runtimeu je @upstash/ratelimit u kombinaciji s @upstash/redis. Upstash nudi Redis kompatibilnu bazu podataka optimiziranu za serverless i edge okruženja s REST API-jem, što je čini savršenom za korištenje u Next.js middlewareu. Za razliku od tradicionalnog Redisa koji zahtijeva TCP konekciju (nedostupnu u Edge Runtimeu), Upstash komunicira putem HTTP-a, što ga čini kompatibilnim s ograničenjima Edge Runtimea.

// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// Create a new ratelimiter using a sliding window algorithm
export const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
  analytics: true,
  prefix: '@upstash/ratelimit',
});

Algoritam klizećeg prozora (sliding window) je posebno koristan jer pruža glatko ograničavanje bez naglih "resetiranja" koja bi mogla omogućiti rafalni promet na granici prozora. Za razliku od fiksnog prozora (fixed window) koji resetira brojač u fiksnim intervalima — što omogućuje korisniku da pošalje duplo više zahtjeva oko granice prozora — klizeći prozor kontinuirano prati zahtjeve u pomičnom vremenskom okviru.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';

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

  // Apply rate limiting only to API routes
  if (pathname.startsWith('/api/')) {
    // Use IP address as identifier (or user ID for authenticated users)
    const ip = request.headers.get('x-forwarded-for') ??
               request.headers.get('x-real-ip') ??
               '127.0.0.1';

    const identifier = `api:${ip}`;

    const { success, limit, reset, remaining } = await ratelimit.limit(
      identifier
    );

    // Add rate limit headers to the response
    const response = success
      ? NextResponse.next()
      : NextResponse.json(
          { error: 'Too many requests. Please try again later.' },
          { status: 429 }
        );

    response.headers.set('X-RateLimit-Limit', limit.toString());
    response.headers.set('X-RateLimit-Remaining', remaining.toString());
    response.headers.set('X-RateLimit-Reset', reset.toString());

    return response;
  }

  return NextResponse.next();
}

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

Napredni rate limiting s različitim razinama

U stvarnim aplikacijama, često trebate različite limite za različite tipove korisnika i ruta. Krajnje točke za autentifikaciju trebaju najstrože limite jer su meta brute force napada, dok opće API rute mogu biti nešto blaže.

Evo kako implementirati stratificirani rate limiting:

// lib/rate-limiters.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

// Strict limiter for auth endpoints (prevent brute force)
export const authLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '60 s'), // 5 attempts per minute
  prefix: 'rl:auth',
});

// Standard limiter for API routes
export const apiLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(30, '10 s'), // 30 requests per 10 seconds
  prefix: 'rl:api',
});

// Generous limiter for page requests
export const pageLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(60, '10 s'), // 60 requests per 10 seconds
  prefix: 'rl:page',
});
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { authLimiter, apiLimiter, pageLimiter } from '@/lib/rate-limiters';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';

  let limiter;
  let identifier;

  if (pathname.startsWith('/api/auth')) {
    limiter = authLimiter;
    identifier = `auth:${ip}`;
  } else if (pathname.startsWith('/api/')) {
    limiter = apiLimiter;
    identifier = `api:${ip}`;
  } else {
    limiter = pageLimiter;
    identifier = `page:${ip}`;
  }

  const { success, limit, remaining, reset } = await limiter.limit(identifier);

  if (!success) {
    return NextResponse.json(
      {
        error: 'Rate limit exceeded',
        retryAfter: Math.ceil((reset - Date.now()) / 1000),
      },
      {
        status: 429,
        headers: {
          'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': reset.toString(),
        },
      }
    );
  }

  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Limit', limit.toString());
  response.headers.set('X-RateLimit-Remaining', remaining.toString());
  return response;
}

Ovaj pristup pruža fino granuliranu kontrolu nad ograničavanjem zahtjeva, s najstrožim limitima na osjetljivim krajnjim točkama poput autentifikacije, gdje su napadi grubom silom najvjerojatniji. Važno je uključiti Retry-After zaglavlje u 429 odgovore jer ono informira klijente koliko trebaju čekati prije ponovnog pokušaja, što je korisno i za legitimne korisnike i za dobro napisane automatizirane klijente.

Geolokacija i A/B testiranje

Jedna od najzanimljivijih mogućnosti Next.js middlewarea je pristup geolokacijskim podacima o korisniku. Kada se vaša aplikacija hostira na platformama poput Vercela, Edge Runtime automatski pruža informacije o zemlji, regiji, gradu i koordinatama korisnika — sve bez dodatnih API poziva ili troškova.

Ovi podaci dolaze iz IP adrese korisnika i geolokacijske baze podataka na edge čvoru, što znači da su dostupni s nultom dodatnom latencijom.

Korištenje geolokacijskih podataka

Geolokacijski podaci su korisni za mnoge scenarije: prikazivanje cijena u lokalnoj valuti, usklađenost s regulativama (poput GDPR-a u EU ili zakona o privatnosti u Kaliforniji), preusmjeravanje na lokalizirane verzije sadržaja ili blokiranje pristupa iz određenih jurisdikcija. Evo praktičnog primjera koji pokriva nekoliko čestih obrazaca:

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

export function middleware(request: NextRequest) {
  const { geo } = request;
  const country = geo?.country || 'US';
  const city = geo?.city || 'Unknown';
  const region = geo?.region || 'Unknown';

  // Add geo data to request headers
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-country', country);
  requestHeaders.set('x-user-city', city);
  requestHeaders.set('x-user-region', region);

  // Block access from specific countries (compliance reasons)
  const blockedCountries = ['XX', 'YY']; // Replace with actual country codes
  if (blockedCountries.includes(country)) {
    return NextResponse.rewrite(new URL('/blocked', request.url));
  }

  // Redirect to country-specific content
  if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
    return NextResponse.redirect(new URL(`/de${request.nextUrl.pathname}`, request.url));
  }

  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}

A/B testiranje s middlewareom

Middleware je idealno mjesto za implementaciju A/B testiranja jer možete korisnicima dodijeliti varijante prije nego što stranica počne renderirati, izbjegavajući "treperenje" sadržaja (content flicker) koje se javlja kod klijentskih A/B rješenja.

Kod klijentskih pristupa, korisnik najprije vidi kontrolnu verziju, a zatim se sadržaj mijenja kada se JavaScript učita — što je loše korisničko iskustvo i može negativno utjecati na rezultate eksperimenta. S middleware pristupom, korisnik odmah dobiva ispravnu varijantu jer se odluka donosi na serveru.

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

const EXPERIMENT_COOKIE = 'ab-experiment-pricing';
const VARIANTS = ['control', 'variant-a', 'variant-b'] as const;

function getVariant(experimentCookie: string | undefined): string {
  if (experimentCookie && VARIANTS.includes(experimentCookie as any)) {
    return experimentCookie;
  }
  // Assign random variant based on weighted distribution
  const random = Math.random();
  if (random < 0.34) return 'control';
  if (random < 0.67) return 'variant-a';
  return 'variant-b';
}

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

  if (pathname === '/pricing') {
    const existingVariant = request.cookies.get(EXPERIMENT_COOKIE)?.value;
    const variant = getVariant(existingVariant);

    // Rewrite to the variant page
    const url = request.nextUrl.clone();
    url.pathname = `/pricing/${variant}`;

    const response = NextResponse.rewrite(url);

    // Set the cookie if it doesn't exist yet
    if (!existingVariant) {
      response.cookies.set(EXPERIMENT_COOKIE, variant, {
        maxAge: 60 * 60 * 24 * 30, // 30 days
        httpOnly: true,
        sameSite: 'lax',
      });
    }

    return response;
  }

  return NextResponse.next();
}

Feature Flags u middlewareu

Middleware možete koristiti i za implementaciju značajki (feature flags) koje se provjeravaju na razini zahtjeva. Feature flags su ključni za postupno uvođenje novih funkcionalnosti — možete ih omogućiti za mali postotak korisnika, za specifične regije ili za beta testere, te ih brzo isključiti ako dođe do problema.

Integracija s vanjskim servisima za feature flags (poput LaunchDarkly, Unleash ili ConfigCat) omogućuje promjenu ponašanja aplikacije bez ponovnog deploymenta:

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

interface FeatureFlags {
  newCheckout: boolean;
  betaDashboard: boolean;
  maintenanceMode: boolean;
}

async function getFeatureFlags(): Promise<FeatureFlags> {
  // Fetch from your feature flag service (e.g., LaunchDarkly, Unleash)
  // Use edge-compatible fetch
  const response = await fetch('https://flags.example.com/api/flags', {
    next: { revalidate: 60 }, // Cache for 60 seconds
  });
  return response.json();
}

export async function middleware(request: NextRequest) {
  const flags = await getFeatureFlags();
  const { pathname } = request.nextUrl;

  // Maintenance mode - redirect all users
  if (flags.maintenanceMode && !pathname.startsWith('/maintenance')) {
    return NextResponse.rewrite(new URL('/maintenance', request.url));
  }

  // New checkout feature flag
  if (pathname.startsWith('/checkout') && flags.newCheckout) {
    return NextResponse.rewrite(
      new URL(pathname.replace('/checkout', '/checkout-v2'), request.url)
    );
  }

  // Beta dashboard - only for beta users
  if (pathname.startsWith('/dashboard') && flags.betaDashboard) {
    const isBetaUser = request.cookies.get('beta-user')?.value === 'true';
    if (isBetaUser) {
      return NextResponse.rewrite(
        new URL(pathname.replace('/dashboard', '/dashboard-beta'), request.url)
      );
    }
  }

  // Pass feature flags to the application via headers
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-feature-flags', JSON.stringify(flags));

  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}

Korištenje middlewarea za feature flags ima prednost nad klijentskim pristupom jer se odluka donosi na serveru, čime se izbjegava nepotrebno učitavanje koda za značajke koje korisnik neće vidjeti. Također, NextResponse.rewrite() je posebno koristan ovdje jer mijenja internu rutu bez promjene URL-a u pregledniku korisnika — korisnik vidi /checkout u adresnoj traci, ali se zapravo poslužuje sadržaj s /checkout-v2.

Internacionalizacija (i18n)

Internacionalizacija je jedan od najčešćih i najkompleksnijih scenarija korištenja middlewarea u Next.js aplikacijama. U globaliziranom web okruženju, podrška za više jezika nije luksuz nego nužnost za bilo koju aplikaciju koja cilja međunarodno tržište.

Middleware vam omogućuje automatsku detekciju korisnikovog preferiranog jezika na temelju postavki preglednika, kolačića ili geolokacije, te preusmjeravanje na odgovarajuću lokaliziranu verziju stranice. Ovo se odvija transparentno, bez ikakve akcije od strane korisnika, pružajući prilagođeno iskustvo od prvog posjeta.

Osnovna detekcija lokalizacije

Osnovni pristup internacionalizaciji uključuje analizu Accept-Language zaglavlja koje preglednik automatski šalje s preferiranim jezicima korisnika, te usporedbu s jezicima koje vaša aplikacija podržava. Biblioteka @formatjs/intl-localematcher koristi standardni IETF BCP 47 algoritam za pronalaženje najboljeg podudaranja:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { match as matchLocale } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

const locales = ['hr', 'en', 'de', 'it', 'sl'];
const defaultLocale = 'hr';

function getLocale(request: NextRequest): string {
  // Check cookie first
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && locales.includes(cookieLocale)) {
    return cookieLocale;
  }

  // Parse Accept-Language header
  const negotiatorHeaders: Record<string, string> = {};
  request.headers.forEach((value, key) => {
    negotiatorHeaders[key] = value;
  });

  const languages = new Negotiator({ headers: negotiatorHeaders }).languages();

  try {
    return matchLocale(languages, locales, defaultLocale);
  } catch {
    return defaultLocale;
  }
}

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

  // Check if the pathname already has a locale prefix
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) {
    return NextResponse.next();
  }

  // Redirect to the correct locale
  const locale = getLocale(request);
  const newUrl = new URL(`/${locale}${pathname}`, request.url);
  newUrl.search = request.nextUrl.search;

  return NextResponse.redirect(newUrl);
}

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

Integracija s next-intl

Biblioteka next-intl je jedna od najpopularnijih i najzrelijih rješenja za internacionalizaciju u Next.js App Routeru. Nudi vlastiti middleware koji elegantno rješava lokalizaciju, uključujući detekciju jezika, upravljanje prefiksima lokalizacije, i integraciju s App Router strukturom direktorija.

Umjesto da sami implementirate svu tu logiku, next-intl je testiran u tisućama produkcijskih aplikacija i pokriva mnoge rubne slučajeve kojih možda niste ni svjesni:

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale, localePrefix } from '@/i18n/config';

export default createMiddleware({
  locales,
  defaultLocale,
  localePrefix, // 'always' | 'as-needed' | 'never'
  localeDetection: true,
});

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
// i18n/config.ts
export const locales = ['hr', 'en', 'de', 'it', 'sl'] as const;
export type Locale = (typeof locales)[number];

export const defaultLocale: Locale = 'hr';

// 'always' - always show locale prefix (/hr/about, /en/about)
// 'as-needed' - hide default locale (/about for hr, /en/about for en)
// 'never' - never show locale prefix (use cookies/headers instead)
export const localePrefix = 'as-needed' as const;

Strategija prefiksa lokalizacije je važna odluka za SEO i korisničko iskustvo. Opcija 'always' osigurava da svaka stranica ima eksplicitan jezični prefiks, što je najjasnije za tražilice i korisnike. Opcija 'as-needed' skriva prefiks za zadani jezik, što daje čišće URL-ove za većinu korisnika. Opcija 'never' potpuno uklanja prefikse i oslanja se na kolačiće ili zaglavlja za određivanje jezika — ovo može komplicirati SEO i dijeljenje linkova, pa se rijetko koristi u praksi.

Kombiniranje i18n s autentifikacijom

U stvarnim aplikacijama, često trebate kombinirati internacionalizaciju s autentifikacijom u istom middlewareu. Ovo može biti izazovno jer oba sustava trebaju obrađivati zahtjev, a redoslijed obrade je važan.

Evo kako to učiniti čisto i organizirano, osiguravajući da se autentifikacija provjeri prva, ali da se lokalizacija primijeni na sve odgovore:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from '@/i18n/config';
import { verifyToken } from '@/lib/auth';

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'as-needed',
});

const protectedPaths = ['/dashboard', '/account', '/settings'];
const authPaths = ['/login', '/register'];

function isProtectedPath(pathname: string): boolean {
  // Remove locale prefix for comparison
  const pathWithoutLocale = locales.reduce(
    (path, locale) => path.replace(`/${locale}`, ''),
    pathname
  ) || '/';

  return protectedPaths.some(p => pathWithoutLocale.startsWith(p));
}

function isAuthPath(pathname: string): boolean {
  const pathWithoutLocale = locales.reduce(
    (path, locale) => path.replace(`/${locale}`, ''),
    pathname
  ) || '/';

  return authPaths.some(p => pathWithoutLocale.startsWith(p));
}

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

  // Step 1: Check authentication for protected routes
  if (isProtectedPath(pathname)) {
    const token = request.cookies.get('auth-token')?.value;
    const payload = token ? await verifyToken(token) : null;

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

  // Step 2: Redirect authenticated users from auth pages
  if (isAuthPath(pathname)) {
    const token = request.cookies.get('auth-token')?.value;
    if (token) {
      const payload = await verifyToken(token);
      if (payload) {
        return NextResponse.redirect(new URL('/dashboard', request.url));
      }
    }
  }

  // Step 3: Apply internationalization middleware
  return intlMiddleware(request);
}

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

Ovaj pristup osigurava da se autentifikacija provjeri prije lokalizacije, ali da se lokalizacija primijeni na sve odgovore, uključujući preusmjeravanja na stranicu za prijavu. Obratite pažnju na pomoćne funkcije isProtectedPath i isAuthPath koje uklanjaju jezični prefiks prije usporedbe putanje — bez ovoga, korisnik na /en/dashboard ne bi bio prepoznat kao netko tko pokušava pristupiti zaštićenoj ruti.

Najbolje prakse i sigurnosna razmatranja

Nakon što smo prošli kroz mnoge obrasce korištenja middlewarea, vrijeme je da objedinimo najvažnije najbolje prakse i sigurnosna razmatranja koja bi trebala voditi vašu implementaciju. Ove prakse nisu samo teorijske preporuke — one su rezultat iskustva cijelih zajednica programera koji su se susreli s problemima u produkciji.

Održavajte middleware laganim

Middleware se izvršava za svaki zahtjev koji odgovara matcheru. To znači da svaka milisekunda dodana u middleware izravno utječe na korisničko iskustvo za svaki zahtjev. Ako vaš middleware traje 200ms, svaka stranica u vašoj aplikaciji bit će 200ms sporija.

Na skali stotina tisuća zahtjeva dnevno, to se brzo akumulira i u troškovima i u degradaciji korisničkog iskustva. Pridržavajte se sljedećih pravila:

  • Izbjegavajte teške računalne operacije — ne obavljajte složeno kriptiranje, kompresiju slika ili intenzivne kalkulacije u middlewareu. Te operacije premjestite u serverske funkcije ili pozadinske zadatke.
  • Minimizirajte mrežne pozive — svaki fetch poziv u middlewareu dodaje latenciju. Koristite keširanje kad god je moguće i izbjegavajte pozive koji nisu apsolutno nužni.
  • Ne uvozite velike biblioteke — veličina middleware bundlea utječe na hladne startove. Uvozite samo ono što vam je stvarno potrebno. Koristite tree-shaking kompatibilne biblioteke.
  • Koristite prednosti Edge Runtimea — Edge Runtime je optimiziran za brze odgovore, ali to zahtijeva da vaš kod bude lagan i efikasan. Razmislite o tome kao o "brzom filtru", ne "teškom procesoru".
// BAD: Heavy operation in middleware
export async function middleware(request: NextRequest) {
  // DON'T: Fetching full user profile from database
  const user = await fetchUserProfile(userId); // Slow!

  // DON'T: Complex data transformation
  const processedData = heavyDataProcessing(user); // CPU intensive!

  return NextResponse.next();
}

// GOOD: Lightweight middleware
export async function middleware(request: NextRequest) {
  // DO: Quick token verification
  const token = request.cookies.get('token')?.value;
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // DO: Verify JWT signature (fast operation)
  const isValid = await jwtVerify(token, secret);
  if (!isValid) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

Nikada se ne oslanjajte isključivo na middleware za sigurnost

Ovo je najvažnija lekcija iz CVE-2025-29927 ranjivosti, ali vrijedi i izvan konteksta te specifične ranjivosti. Middleware je prvi sloj obrane, ali nikada ne smije biti jedini.

Čak i bez poznatih ranjivosti, uvijek postoji mogućnost novih propusta, grešaka u konfiguraciji ili promjena ponašanja pri ažuriranju frameworka:

  • Provjeravajte autentifikaciju u Server Componentima — svaka stranica koja prikazuje osjetljive podatke trebala bi neovisno provjeriti sesiju korištenjem getServerSession() ili ekvivalentne funkcije.
  • Provjeravajte autorizaciju u API rutama — svaka API ruta mora neovisno verificirati da korisnik ima pravo pristupa resursu. Nikada ne pretpostavljajte da je middleware već obavio provjeru.
  • Implementirajte Data Access Layer (DAL) — centralizirani sloj pristupa podacima koji uključuje provjere autorizacije. Ovo osigurava da se podaci filtriraju ispravno bez obzira na to kako se pristupa.
  • Koristite RBAC ili ABAC modele — implementirajte formalne modele kontrole pristupa, ne ad-hoc provjere raspršene po kodu. Formalni modeli su lakši za audit i manje podložni propustima.

Ispravno rukovanje pogreškama

Middleware ne bi trebao nikada "tiho" pasti ili propuštati zahtjeve kada dođe do neočekivane pogreške. Princip "fail secure" (sigurno zakazivanje) nalaže da u slučaju pogreške trebate pretpostaviti najgori scenarij i blokirati pristup, radije nego ga dopustiti.

Ako dođe do pogreške pri verifikaciji tokena, bolje je preusmjeriti korisnika na stranicu za prijavu nego mu dopustiti pristup zaštićenom resursu:

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

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

    const payload = await verifyToken(token);

    if (!payload) {
      // Token is invalid - clear it and redirect
      const response = NextResponse.redirect(new URL('/login', request.url));
      response.cookies.delete('token');
      return response;
    }

    return NextResponse.next();
  } catch (error) {
    // Log the error for debugging
    console.error('[Middleware Error]', error);

    // In case of unexpected errors, fail securely
    // Don't expose the protected resource - redirect to a safe page
    return NextResponse.redirect(new URL('/error', request.url));
  }
}

Koristite sigurnosna zaglavlja

Middleware je izvrsno mjesto za dodavanje sigurnosnih HTTP zaglavlja na sve odgovore. Ova zaglavlja ne zahtijevaju nikakvu logiku — jednostavno se dodaju na svaki odgovor — ali pružaju značajan sloj zaštite protiv čestih napada poput clickjackinga, XSS-a i MIME sniffinga:

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

  // Security headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set('X-XSS-Protection', '1; mode=block');
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=(self)'
  );

  // Content Security Policy
  const csp = [
    "default-src 'self'",
    "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self' https://api.example.com",
    "frame-ancestors 'none'",
  ].join('; ');

  response.headers.set('Content-Security-Policy', csp);

  return response;
}

Testiranje middlewarea

Middleware bi trebao biti testiran kao svaki drugi dio vaše aplikacije — i to rigorozno, jer greška u middlewareu može kompromitirati sigurnost cijele aplikacije. Evo primjera kako testirati middleware koristeći jednostavne unit testove s Jest frameworkom:

// __tests__/middleware.test.ts
import { middleware } from '@/middleware';
import { NextRequest } from 'next/server';

function createMockRequest(
  url: string,
  options: { cookies?: Record<string, string>; headers?: Record<string, string> } = {}
) {
  const request = new NextRequest(new URL(url, 'http://localhost:3000'));

  if (options.cookies) {
    Object.entries(options.cookies).forEach(([name, value]) => {
      request.cookies.set(name, value);
    });
  }

  if (options.headers) {
    Object.entries(options.headers).forEach(([name, value]) => {
      request.headers.set(name, value);
    });
  }

  return request;
}

describe('Middleware', () => {
  it('should redirect unauthenticated users to login', async () => {
    const request = createMockRequest('/dashboard');
    const response = await middleware(request);

    expect(response.status).toBe(307);
    expect(response.headers.get('location')).toContain('/login');
  });

  it('should allow authenticated users to access protected routes', async () => {
    const request = createMockRequest('/dashboard', {
      cookies: { 'auth-token': 'valid-jwt-token' },
    });
    const response = await middleware(request);

    expect(response.status).toBe(200);
  });

  it('should allow access to public routes without authentication', async () => {
    const request = createMockRequest('/login');
    const response = await middleware(request);

    expect(response.status).toBe(200);
  });
});

Organizacija složenog middlewarea

Kada vaš middleware počne rasti i pokrivati više odgovornosti — autentifikaciju, rate limiting, lokalizaciju, sigurnosna zaglavlja — važno ga je dobro organizirati kako ne bi postao nečitljiv monolit.

Evo jednog provjerenog pristupa koji razdvaja brige u zasebne module, a zatim ih komponira u jedinstven middleware lanac:

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

export async function authMiddleware(request: NextRequest): Promise<NextResponse | null> {
  const token = request.cookies.get('token')?.value;
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  // Return null to indicate "continue to next middleware"
  return null;
}

// lib/middleware/rate-limit.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';

export async function rateLimitMiddleware(request: NextRequest): Promise<NextResponse | null> {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json({ error: 'Rate limited' }, { status: 429 });
  }
  return null;
}

// lib/middleware/security-headers.ts
import { NextResponse } from 'next/server';

export function addSecurityHeaders(response: NextResponse): NextResponse {
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  return response;
}

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { authMiddleware } from '@/lib/middleware/auth';
import { rateLimitMiddleware } from '@/lib/middleware/rate-limit';
import { addSecurityHeaders } from '@/lib/middleware/security-headers';

export async function middleware(request: NextRequest) {
  // Run middleware chain
  const rateLimitResponse = await rateLimitMiddleware(request);
  if (rateLimitResponse) return rateLimitResponse;

  const authResponse = await authMiddleware(request);
  if (authResponse) return authResponse;

  // If all checks pass, continue with security headers
  const response = NextResponse.next();
  return addSecurityHeaders(response);
}

Ovaj modularni pristup čini vaš middleware čitljivijim, lakšim za testiranje i jednostavnijim za održavanje dok vaša aplikacija raste. Svaki modul se može testirati neovisno, a novi moduli se mogu dodati u lanac bez utjecaja na postojeće. Konvencija vraćanja null za "nastavi dalje" jasno razdvaja slučajeve kada middleware želi preuzeti kontrolu nad odgovorom od slučajeva kada želi jednostavno propustiti zahtjev dalje.

Zaključak

Next.js middleware je iznimno moćan alat koji, kada se koristi ispravno, može značajno poboljšati sigurnost, performanse i korisničko iskustvo vaše aplikacije. Kroz ovaj vodič smo prošli od osnovnih koncepata do naprednih obrazaca, pokrivajući autentifikaciju s JWT-om i Auth.js-om, rate limiting s Upstash Redisom, geolokaciju, A/B testiranje s rewrite pristupom, feature flags i internacionalizaciju s next-intl bibliotekom.

No, možda najvažnija lekcija ovog vodiča nije o implementaciji specifičnih obrazaca, nego o razumijevanju ograničenja middlewarea. CVE-2025-29927 ranjivost nam je pokazala da se nijedan pojedinačni sigurnosni mehanizam ne smije smatrati neprobojnim, bez obzira na to koliko se čini robustan.

Obrana u dubinu — višeslojni pristup sigurnosti gdje svaki sloj neovisno štiti vašu aplikaciju — nije samo najbolja praksa, već apsolutna nužnost za bilo koju produkcijsku aplikaciju koja rukuje osjetljivim podacima.

Evo ključnih zaključaka koje biste trebali ponijeti sa sobom:

  • Middleware je prvi, ali ne i jedini sloj zaštite — uvijek implementirajte provjere autentifikacije i autorizacije na razini stranica (Server Components), API ruta i pristupa podacima (Data Access Layer). Tretirajte middleware kao korisnu optimizaciju, ne kao sigurnosni temelj.
  • Držite middleware laganim — izbjegavajte teške operacije, minimizirajte mrežne pozive i uvozite samo nužne biblioteke. Svaka milisekunda u middlewareu utječe na svaki zahtjev u vašoj aplikaciji.
  • Ažurirajte redovito — sigurnosne ranjivosti se otkrivaju kontinuirano. Držite Next.js i sve ovisnosti ažurnima, pratite sigurnosne obavijesti i testirajte ažuriranja u staging okruženju prije produkcije.
  • Testirajte middleware — kao i svaki drugi kritični dio aplikacije, middleware zaslužuje temeljite unit testove i integracijske testove. Posebno testirajte rubne slučajeve poput isteklih tokena, nepostojećih kolačića i neočekivanih pogrešaka.
  • Koristite Edge Runtime svjesno — razumijte ograničenja Edge Runtimea i birajte biblioteke koje su s njim kompatibilne. Testirajte lokalno jer se neke razlike između Node.js i Edge Runtimea ne vide u razvoju.
  • Pratite performanse — middleware utječe na svaki zahtjev, pa i male degradacije performansi mogu imati znatan učinak na korisničko iskustvo. Implementirajte praćenje trajanja izvršavanja i postavite alarme za spore middleware izvršavanja.

Middleware u Next.js-u nastavlja evoluirati sa svakom novom verzijom frameworka. Kako se ekosustav razvija, tako se pojavljuju i novi obrasci, mogućnosti i najbolje prakse. Ono što ostaje konstantno je potreba za razumijevanjem osnova — kako middleware funkcionira, gdje se nalazi u životnom ciklusu zahtjeva, koje su njegove prednosti i ograničenja, te zašto nikada ne smije biti jedina linija obrane vaše aplikacije.

S tim znanjem i praktičnim primjerima iz ovog vodiča, spremni ste izgraditi sigurne, brze i skalabilne Next.js aplikacije koje pravilno koriste middleware kao moćan, ali svjesno ograničen alat u vašem razvojnom arsenalu.

O Autoru Editorial Team

Our team of expert writers and editors.