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:
- 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).
- 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.
- 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. - 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,
fsmodul nije dostupan, kao ni mnogi drugi Node.js specifični API-ji poputchild_process,net, ilidgram. Također nemaste pristup globalnim varijablama specifičnim za Node.js poput__dirnameiliprocess.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,Responsei 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
fetchpoziv 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.