Εισαγωγή: Τι Είναι το Middleware στο Next.js και Γιατί Πρέπει να το Γνωρίζεις
Αν δουλεύεις με Next.js App Router, υπάρχει ένα εργαλείο που τρέχει πριν από κάθε request — πριν φτάσει στη σελίδα, πριν εκτελεστεί κάποιο Server Component, πριν ακόμα επιστρέψει δεδομένα ένα API route. Λέγεται Middleware και, ειλικρινά, είναι ένα από τα πιο ισχυρά χαρακτηριστικά του framework.
Σκέψου το σαν έναν φρουρό στην πόρτα. Κάθε αίτημα περνάει πρώτα από αυτόν: Είναι αυθεντικοποιημένος ο χρήστης; Σε ποια γλώσσα θα δει τη σελίδα; Έχει ξεπεράσει το rate limit; Πρέπει να γίνει redirect;
Σε αυτόν τον οδηγό θα δούμε πώς να χρησιμοποιήσεις το Middleware για authentication, rate limiting, internationalization (i18n), Content Security Policy (CSP), geolocation και A/B testing. Θα δούμε πραγματικά παραδείγματα κώδικα — και θα μιλήσουμε κι για μια κρίσιμη ευπάθεια που ίσως δεν ξέρεις ακόμα.
Βασική Δομή και Ρύθμιση του Middleware
Δημιουργία του Αρχείου middleware.ts
Το Middleware ορίζεται σε ένα μοναδικό αρχείο στη ρίζα του project — στο ίδιο επίπεδο με τους φακέλους app ή pages. Ναι, μόνο ένα αρχείο middleware ανά project. Αυτό μπορεί να φαίνεται περιοριστικό στην αρχή, αλλά (όπως θα δούμε παρακάτω) υπάρχουν καθαρά patterns για να οργανώσεις πολλαπλές λειτουργίες.
// middleware.ts (ρίζα του project)
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Η λογική σου εδώ — τρέχει σε κάθε matched request
// Επιστρέφεις NextResponse.next() για να συνεχιστεί η ροή
return NextResponse.next();
}
// Ορίζεις ποια routes θα ενεργοποιούν το middleware
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};
Πώς Λειτουργεί Εσωτερικά
Η ροή ενός request είναι πολύ απλή:
- Ο browser στέλνει HTTP request
- Το Middleware εκτελείται πρώτο — πριν από routing, layout rendering ή data fetching
- Μπορεί να τροποποιήσει headers, να κάνει redirect/rewrite, ή να επιστρέψει custom response
- Αν επιστρέψει
NextResponse.next(), το request συνεχίζει κανονικά
Μέχρι πρόσφατα, το Middleware εκτελούνταν αποκλειστικά στο Edge Runtime — ένα ελαφρύ JavaScript runtime βασισμένο στο V8. Γρήγορο, ναι, αλλά με περιορισμούς: δεν μπορούσες να χρησιμοποιήσεις βιβλιοθήκες που εξαρτώνταν από native Node.js modules. Κι αυτό ήταν αρκετά ενοχλητικό σε πολλές περιπτώσεις.
Σημαντική Αλλαγή: Node.js Runtime στο Middleware (Next.js 15.5)
Με τη κυκλοφορία του Next.js 15.5, η υποστήριξη Node.js runtime στο Middleware έγινε stable. Πρακτικά; Μπορείς πλέον να χρησιμοποιήσεις ολόκληρο το Node.js API — modules όπως fs, crypto, καθώς και npm packages που απαιτούν Node.js:
// middleware.ts — με Node.js runtime
import { NextRequest, NextResponse } from "next/server";
export const config = {
runtime: "nodejs", // Πλέον stable!
matcher: ["/dashboard/:path*"],
};
export function middleware(request: NextRequest) {
// Τώρα μπορείς να χρησιμοποιήσεις Node.js APIs
// π.χ. native crypto, database drivers, κλπ.
return NextResponse.next();
}
Αξίζει να σημειωθεί ότι στο Next.js 16, το αρχείο middleware.ts μετονομάζεται σε proxy.ts. Δεν είναι απλά αλλαγή ονόματος — αντικατοπτρίζει τον ρόλο του ως network boundary. Και στο Next.js 16, χρησιμοποιεί αποκλειστικά Node.js runtime, χωρίς εναλλακτική.
Matcher: Ελέγχοντας Πού Τρέχει το Middleware
Η ιδιότητα matcher καθορίζει ποια URL paths ενεργοποιούν το Middleware. Υποστηρίζει glob patterns, κάτι αρκετά βολικό:
export const config = {
matcher: [
// Ταίριασμα συγκεκριμένων paths
"/dashboard/:path*",
"/api/:path*",
// Εξαίρεση στατικών αρχείων και εικόνων
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
Εναλλακτικά, μπορείς να ελέγξεις conditionally μέσα στη συνάρτηση:
export function middleware(request: NextRequest) {
// Πέρνα μόνο τα paths που σε ενδιαφέρουν
if (request.nextUrl.pathname.startsWith("/public")) {
return NextResponse.next();
}
// Εφάρμοσε λογική στα υπόλοιπα
return checkAuth(request);
}
Authentication στο Middleware: Στρατηγικές και Ασφάλεια
Βασικό Pattern: Έλεγχος Session Cookie
Ας ξεκινήσουμε με το πιο κοινό pattern: έλεγχος ύπαρξης session cookie. Η ιδέα είναι απλή — αν ο χρήστης δεν έχει έγκυρο session, τον στέλνεις στο login:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const protectedRoutes = ["/dashboard", "/profile", "/settings"];
const authRoutes = ["/login", "/register"];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const sessionToken = request.cookies.get("session-token")?.value;
// Αν ο χρήστης προσπαθεί να μπει σε protected route χωρίς session
if (protectedRoutes.some((route) => pathname.startsWith(route))) {
if (!sessionToken) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
}
// Αν είναι ήδη authenticated, μην τον αφήνεις στο login
if (authRoutes.some((route) => pathname.startsWith(route))) {
if (sessionToken) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*", "/settings/:path*", "/login", "/register"],
};
Πρόσεξε το callbackUrl — είναι μια μικρή λεπτομέρεια που κάνει τεράστια διαφορά στο UX. Ο χρήστης γυρνάει ακριβώς εκεί που ήθελε μετά το login.
JWT Verification στο Middleware
Αν χρησιμοποιείς JWT tokens, μπορείς να τα επαληθεύεις απευθείας στο Middleware. Για Edge Runtime, η βιβλιοθήκη jose είναι η go-to επιλογή γιατί χρησιμοποιεί Web Crypto APIs:
// lib/jwt-edge.ts — JWT verification συμβατό με Edge Runtime
import { jwtVerify, type JWTPayload } from "jose";
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function verifyToken(token: string): Promise<JWTPayload | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return payload;
} catch {
return null;
}
}
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyToken } from "@/lib/jwt-edge";
export async function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
const payload = await verifyToken(token);
if (!payload) {
// Token expired ή invalid
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("auth-token");
return response;
}
// Πέρασε user info στα headers για downstream components
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 },
});
}
Ένα πολύ χρήσιμο trick εδώ: βάζεις user info στα request headers, κι έτσι τα Server Components μπορούν να τα διαβάσουν χωρίς να κάνουν ξανά verification. Εξοικονομεί χρόνο.
Role-Based Access Control (RBAC)
Για εφαρμογές με πολλαπλούς ρόλους (admin, editor, viewer κλπ.), μπορείς να εφαρμόσεις RBAC απευθείας στο Middleware:
// Ορισμός permissions ανά role
const rolePermissions: Record<string, string[]> = {
admin: ["/admin", "/dashboard", "/settings"],
editor: ["/dashboard", "/editor"],
viewer: ["/dashboard"],
};
export async function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
const payload = await verifyToken(token);
if (!payload) {
return NextResponse.redirect(new URL("/login", request.url));
}
const userRole = payload.role as string;
const { pathname } = request.nextUrl;
// Έλεγχος αν ο ρόλος έχει πρόσβαση στο συγκεκριμένο path
const allowedPaths = rolePermissions[userRole] || [];
const hasAccess = allowedPaths.some((path) => pathname.startsWith(path));
if (!hasAccess) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
return NextResponse.next();
}
Απλό, καθαρό και λειτουργεί. Για πιο πολύπλοκα permission models (π.χ. resource-level permissions), θα χρειαστείς μάλλον κάτι πιο εξελιγμένο — αλλά για τις περισσότερες εφαρμογές, αυτό αρκεί μια χαρά.
Κρίσιμη Ευπάθεια: CVE-2025-29927
Εδώ θέλω να σταθώ λίγο παραπάνω, γιατί είναι σοβαρό θέμα.
Τον Μάρτιο 2025, αποκαλύφθηκε μια κρίσιμη ευπάθεια (CVE-2025-29927) που επέτρεπε σε επιτιθέμενους να παρακάμψουν πλήρως τους ελέγχους του Middleware μέσω χειραγώγησης του header x-middleware-subrequest. Σκέψου το: αν βασιζόσουν αποκλειστικά στο Middleware για authentication, η εφαρμογή σου ήταν ανοιχτή.
Τι πρέπει να κάνεις:
- Αναβάθμισε αμέσως σε Next.js 15.2.3+, 14.2.25+, 13.5.9+ ή 12.3.5+
- Αν κάνεις deploy σε Vercel ή Netlify, ήσουν ασφαλής — αυτές οι πλατφόρμες φιλτράρουν αυτόματα ύποπτα headers
- Self-hosted εφαρμογές ήταν ευάλωτες και χρειάζονταν άμεση ενημέρωση
Defense-in-Depth: Μην Βασίζεσαι Μόνο στο Middleware
Αυτή η ευπάθεια υπογράμμισε κάτι που πολλοί developers ξεχνούν: μην βασίζεσαι ποτέ σε ένα μοναδικό σημείο ελέγχου. Η σωστή προσέγγιση είναι defense-in-depth — δηλαδή, επαλήθευση authentication σε κάθε layer:
// Middleware — πρώτο layer (optimistic check)
// ✅ Ελέγχει cookie ύπαρξη — γρήγορο redirect
// Server Component — δεύτερο layer
import { cookies } from "next/headers";
import { verifySession } from "@/lib/auth";
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get("auth-token")?.value;
const session = await verifySession(token);
if (!session) {
redirect("/login");
}
// Πλήρης επαλήθευση session από τη βάση δεδομένων
const user = await db.user.findUnique({
where: { id: session.userId },
});
return <Dashboard user={user} />;
}
// Server Action — τρίτο layer
"use server";
export async function updateProfile(formData: FormData) {
const session = await getAuthenticatedSession();
if (!session) {
throw new Error("Unauthorized");
}
// Κάθε mutation επαληθεύει ξεχωριστά
await db.user.update({
where: { id: session.userId },
data: { name: formData.get("name") as string },
});
}
Με λίγα λόγια: το Middleware λειτουργεί ως βελτιστοποίηση (γρήγορο redirect αν λείπει το cookie), αλλά η πραγματική ασφάλεια εφαρμόζεται παντού — Server Components, Server Actions, API routes. Κάθε σημείο πρόσβασης δεδομένων πρέπει να επαληθεύει μόνο του.
Rate Limiting: Προστασία Από Κατάχρηση
Γιατί Rate Limiting στο Middleware;
Η λογική είναι straightforward: αν σταματήσεις τα κακόβουλα requests πριν φτάσουν στο backend, γλιτώνεις φόρτο στη βάση δεδομένων, αποτρέπεις brute-force attacks και αξιοποιείς καλύτερα τους πόρους σου.
Rate Limiting με Upstash Redis
Η πιο δημοφιλής λύση — και κατά τη γνώμη μου η πιο κομψή — είναι ο συνδυασμός @upstash/ratelimit με @upstash/redis. Serverless Redis που δουλεύει τέλεια στο Edge:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Δημιουργία rate limiter — sliding window algorithm
// 10 requests ανά 30 δευτερόλεπτα ανά IP
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "30 s"),
analytics: true,
});
export async function middleware(request: NextRequest) {
// Εφάρμοσε rate limiting μόνο σε API routes
if (request.nextUrl.pathname.startsWith("/api")) {
const ip = request.ip ?? request.headers.get("x-forwarded-for") ?? "127.0.0.1";
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Υπερβήκατε το όριο αιτημάτων. Δοκιμάστε ξανά αργότερα." },
{
status: 429,
headers: {
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
}
);
}
}
return NextResponse.next();
}
export const config = {
matcher: "/api/:path*",
};
Rate Limiting Χωρίς External Dependencies
Δεν θέλεις εξωτερική υπηρεσία; Κατανοητό. Μπορείς να φτιάξεις ένα απλό in-memory rate limiter. Αλλά πρόσεξε — αυτή η λύση δουλεύει μόνο σε single-instance deployments, γιατί κάθε instance κρατάει το δικό του Map:
// lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; lastReset: number }>();
export function rateLimit(ip: string, limit: number = 10, windowMs: number = 60000): boolean {
const now = Date.now();
const record = rateLimitMap.get(ip);
if (!record || now - record.lastReset > windowMs) {
rateLimitMap.set(ip, { count: 1, lastReset: now });
return true; // Επιτρέπεται
}
if (record.count >= limit) {
return false; // Blocked
}
record.count++;
return true;
}
// middleware.ts
import { rateLimit } from "@/lib/rate-limit";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/auth")) {
const ip = request.ip ?? "127.0.0.1";
if (!rateLimit(ip, 5, 60000)) {
return NextResponse.json(
{ error: "Πάρα πολλές προσπάθειες σύνδεσης" },
{ status: 429 }
);
}
}
return NextResponse.next();
}
Για production με πολλαπλά instances, ωστόσο, θα χρειαστείς σίγουρα Redis ή κάποιο shared store.
Internationalization (i18n) με Middleware
Αυτόματη Ανίχνευση Γλώσσας
Ένα από τα πιο κλασικά use cases. Ο χρήστης μπαίνει στο site, το Middleware ελέγχει τον Accept-Language header (ή ένα αποθηκευμένο cookie) και τον στέλνει στη σωστή γλώσσα:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
const locales = ["el", "en", "de", "fr"];
const defaultLocale = "el";
function getLocale(request: NextRequest): string {
// Πρώτα, έλεγξε αν υπάρχει cookie προτίμησης
const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value;
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale;
}
// Αλλιώς, χρησιμοποίησε τα Accept-Language headers
const headers: Record<string, string> = {};
request.headers.forEach((value, key) => {
headers[key] = value;
});
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Έλεγξε αν υπάρχει ήδη locale prefix
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) {
return NextResponse.next();
}
// Ανακατεύθυνε στο σωστό locale
const locale = getLocale(request);
const newUrl = new URL(`/${locale}${pathname}`, request.url);
return NextResponse.redirect(newUrl);
}
export const config = {
matcher: ["/((?!_next|api|favicon.ico|.*\\..*).*)" ],
};
Χρήση next-intl για Πλήρη i18n
Αν θέλεις μια πιο ολοκληρωμένη λύση (και ειλικρινά, τις περισσότερες φορές αυτό θέλεις), η βιβλιοθήκη next-intl κάνει τα πράγματα πολύ πιο εύκολα — locale detection, URL rewrites και cookie management, όλα out of the box:
// i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["el", "en", "de"],
defaultLocale: "el",
localePrefix: "as-needed", // Δεν εμφανίζεται prefix για default locale
});
// middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: ["/((?!api|trpc|_next|_vercel|.*\\..*).*)" ],
};
Πέντε γραμμές κώδικα στο middleware. Αυτό είναι. Η next-intl χρησιμοποιεί εσωτερικά τη βιβλιοθήκη @formatjs/intl-localematcher για ακριβή αντιστοίχιση γλώσσας βάσει browser headers.
Content Security Policy (CSP) με Middleware
Γιατί CSP;
Η Content Security Policy προστατεύει την εφαρμογή σου από XSS attacks, clickjacking και code injection. Ορίζει ποιες πηγές επιτρέπεται να φορτώσουν scripts, styles, εικόνες, fonts και λοιπούς πόρους. Αν δεν το χρησιμοποιείς ήδη, αξίζει σοβαρά να το εξετάσεις.
Nonce-Based CSP στο Middleware
Ένα nonce (number used once) είναι ένα μοναδικό, τυχαίο string που δημιουργείται σε κάθε request. Επιτρέπει σε συγκεκριμένα inline scripts και styles να εκτελεστούν, ενώ μπλοκάρει τα υπόλοιπα. Και το Middleware είναι το τέλειο μέρος για τη δημιουργία του:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Δημιουργία μοναδικού nonce
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
// Κατασκευή CSP header
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
const cspValue = cspHeader.replace(/\s{2,}/g, " ").trim();
// Πέρνα το nonce στα request headers (για χρήση στα components)
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", cspValue);
// Πρόσθεσε CSP στο response
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set("Content-Security-Policy", cspValue);
return response;
}
Στα Server Components, διαβάζεις το nonce από τα headers:
// app/layout.tsx
import { headers } from "next/headers";
import Script from "next/script";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const headersList = await headers();
const nonce = headersList.get("x-nonce") ?? "";
return (
<html>
<body>
{children}
<Script
src="https://analytics.example.com/script.js"
nonce={nonce}
strategy="afterInteractive"
/>
</body>
</html>
);
}
Μια σημαντική παγίδα: Η χρήση nonce-based CSP αναγκάζει όλες τις σελίδες σε dynamic rendering — κάθε request χρειάζεται νέο nonce. Αυτό σημαίνει ότι ISR και static caching δεν λειτουργούν. Αν χρειάζεσαι static optimization, κοίτα hash-based CSP ή Subresource Integrity (SRI) ως εναλλακτική.
Geolocation και A/B Testing
Geolocation-Based Routing
Το Middleware έχει πρόσβαση σε geolocation data (κυρίως σε Vercel/edge deployments). Αυτό σου ανοίγει αρκετές δυνατότητες — διαφορετικό content, geo-blocking, ή redirects βάσει τοποθεσίας:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const BLOCKED_COUNTRIES = ["XX", "YY"]; // Χώρες προς αποκλεισμό
export function middleware(request: NextRequest) {
const country = request.geo?.country || "US";
const city = request.geo?.city || "Unknown";
const region = request.geo?.region || "Unknown";
// Geo-blocking
if (BLOCKED_COUNTRIES.includes(country)) {
return new NextResponse("Η υπηρεσία δεν είναι διαθέσιμη στη χώρα σας.", {
status: 451,
});
}
// Πέρνα geolocation info στα headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-user-country", country);
requestHeaders.set("x-user-city", city);
// Country-specific redirects
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 Testing με Cookies
Εδώ τα πράγματα γίνονται ενδιαφέροντα. Το A/B testing στο Middleware είναι ιδανικό γιατί η ανάθεση variant γίνεται πριν τη σελίδα renders — κανένα content flash, κανένα flickering:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const EXPERIMENT_COOKIE = "ab-experiment-hero";
const VARIANTS = ["control", "variant-a", "variant-b"];
export function middleware(request: NextRequest) {
// Μόνο για τη σελίδα που θέλουμε A/B test
if (request.nextUrl.pathname !== "/") {
return NextResponse.next();
}
// Έλεγξε αν ο χρήστης έχει ήδη variant
let variant = request.cookies.get(EXPERIMENT_COOKIE)?.value;
if (!variant || !VARIANTS.includes(variant)) {
// Τυχαία ανάθεση variant
const randomIndex = Math.floor(Math.random() * VARIANTS.length);
variant = VARIANTS[randomIndex];
}
// Rewrite στη σωστή variant σελίδα (αόρατα για τον χρήστη)
const url = request.nextUrl.clone();
url.pathname = `/experiments/hero/${variant}`;
const response = NextResponse.rewrite(url);
// Αποθήκευσε variant σε cookie (30 ημέρες)
response.cookies.set(EXPERIMENT_COOKIE, variant, {
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: "lax",
});
return response;
}
Ο χρήστης βλέπει πάντα / στο URL αλλά λαμβάνει διαφορετική version. Δεν αντιλαμβάνεται τίποτα — κι εσύ μαζεύεις data.
Σύνθεση Πολλαπλών Middleware Λειτουργιών
Chain Pattern
Αφού το Next.js υποστηρίζει μόνο ένα middleware αρχείο, πρέπει να βρεις τρόπο να συνθέσεις πολλαπλές λειτουργίες. Ένα pattern που δουλεύει πολύ καλά στην πράξη είναι η δημιουργία μιας chain function:
// lib/middleware-chain.ts
import { NextRequest, NextResponse } from "next/server";
type MiddlewareFunction = (
request: NextRequest,
response: NextResponse
) => NextResponse | Response | Promise<NextResponse | Response>;
export function chain(...middlewares: MiddlewareFunction[]) {
return async function (request: NextRequest) {
let response = NextResponse.next();
for (const mw of middlewares) {
const result = await mw(request, response);
// Αν επιστράφηκε redirect/rewrite, σταμάτα
if (result instanceof Response && result.status !== 200) {
return result;
}
if (result instanceof NextResponse) {
response = result;
}
}
return response;
};
}
// middleware.ts
import { chain } from "@/lib/middleware-chain";
import { withAuth } from "@/lib/middleware/auth";
import { withRateLimit } from "@/lib/middleware/rate-limit";
import { withI18n } from "@/lib/middleware/i18n";
import { withCSP } from "@/lib/middleware/csp";
export default chain(withRateLimit, withAuth, withI18n, withCSP);
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)" ],
};
Αυτή η προσέγγιση κρατά τον κώδικα οργανωμένο, κάθε middleware function μπορεί να δοκιμαστεί ανεξάρτητα, και η σειρά εκτέλεσης είναι ξεκάθαρη.
Headers Ασφαλείας: Πέρα από το CSP
Εκτός από CSP, το Middleware είναι ιδανικό για να προσθέσεις security headers σε κάθε response. Αυτά τα headers δεν κοστίζουν σχεδόν τίποτα σε performance, αλλά κάνουν σημαντική διαφορά στην ασφάλεια:
// lib/middleware/security-headers.ts
import { NextRequest, NextResponse } from "next/server";
export function withSecurityHeaders(request: NextRequest, response: NextResponse) {
// Αποτρέπει MIME type sniffing
response.headers.set("X-Content-Type-Options", "nosniff");
// Αποτρέπει clickjacking
response.headers.set("X-Frame-Options", "DENY");
// Ελέγχει τι πληροφορίες στέλνονται ως Referer
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
// Περιορίζει πρόσβαση σε browser APIs
response.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=(), browsing-topics=()"
);
// Strict Transport Security (HSTS)
response.headers.set(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload"
);
return response;
}
Performance: Κρατώντας το Middleware Γρήγορο
Best Practices για Απόδοση
Να θυμάσαι: το Middleware τρέχει σε κάθε matched request. Αν αργεί, αργεί τα πάντα. Μερικά πράγματα που πρέπει να προσέχεις:
- Κράτα τη λογική minimal — αποφύγε database calls και heavy computations στο Edge Runtime
- Χρησιμοποίησε cookie-based checks αντί για full session validation — η πλήρης επαλήθευση ας γίνει στα Server Components
- Φιλτράρισε με matcher — μην τρέχεις middleware σε static assets,
_next, εικόνες κλπ. - Αποφύγε external HTTP calls στο Edge Runtime — αν τα χρειάζεσαι, πήγαινε σε Node.js runtime
- Cache στρατηγικά — βιβλιοθήκες όπως
@upstash/ratelimitκάνουν cache αυτόματα όσο η edge function είναι «hot»
Μέτρηση Overhead
Σε μια καλά βελτιστοποιημένη εφαρμογή, το overhead πρέπει να είναι μονοψήφια milliseconds. Αν δεις αύξηση στο TTFB, τσέκαρε:
- Μήπως κάνεις external calls στο middleware
- Μήπως τα matcher patterns είναι πολύ ευρέα
- Μήπως φορτώνεις βαριές βιβλιοθήκες που δεν κάνουν tree-shake
Μετάβαση στο Next.js 16: Από middleware.ts σε proxy.ts
Σχεδιάζεις αναβάθμιση στο Next.js 16; Τότε πρέπει να ξέρεις ότι το middleware.ts γίνεται proxy.ts. Και δεν πρόκειται μόνο για rebranding:
- Το
proxy.tsτρέχει αποκλειστικά με Node.js runtime — τέλος το Edge Runtime - Ο ρόλος του αρχείου γίνεται πιο ξεκάθαρος: network boundary layer και routing proxy
- Η σύνταξη και τα APIs παραμένουν σε μεγάλο βαθμό παρόμοια
// Next.js 15: middleware.ts
// Next.js 16: proxy.ts
import { NextRequest, NextResponse } from "next/server";
// Η ίδια λογική, νέο αρχείο
export default function proxy(request: NextRequest) {
// Πλέον τρέχει πάντα σε Node.js runtime
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next|favicon.ico).*)" ],
};
Αν τρέχεις Next.js 15.5, θα δεις ήδη deprecation warnings. Μην τα αγνοείς — η μετάβαση είναι σχετικά painless αν προετοιμαστείς από τώρα.
Checklist Ασφαλείας Middleware
Πριν κλείσουμε, ένα σύντομο checklist που αξίζει να κρατήσεις κοντά σου:
- Αναβάθμιση: Βεβαιώσου ότι χρησιμοποιείς Next.js 15.2.3+ (patch για CVE-2025-29927)
- Defense-in-depth: Επαλήθευε authentication σε κάθε σημείο πρόσβασης, όχι μόνο στο Middleware
- Input validation: Χρησιμοποίησε schema validation (π.χ. Zod) στα Server Actions
- Rate limiting: Εφάρμοσε rate limiting στα authentication endpoints
- Secure cookies: Πάντα HttpOnly, Secure, SameSite
- CSRF protection: Ενεργοποίησε CSRF protection για state-changing operations
- Security headers: CSP, HSTS, X-Frame-Options, Permissions-Policy
- Request memoization: Χρησιμοποίησε
React.cache()για αποφυγή duplicate auth checks στο ίδιο request - Monitoring: Ρύθμισε CSP violation reporting (π.χ. μέσω Sentry)
- Testing: Γράψε tests για τα middleware patterns — ειδικά τα auth flows
Συμπέρασμα
Το Middleware στο Next.js είναι πολύ παραπάνω από ένα απλό φίλτρο. Είναι ένα στρατηγικό σημείο ελέγχου όπου εφαρμόζεις authentication, rate limiting, i18n, CSP, geolocation routing και A/B testing — όλα πριν αρχίσει καν η σελίδα να renders.
Με τη μετάβαση στο Node.js runtime και τη μετονομασία σε proxy.ts στο Next.js 16, το εργαλείο γίνεται ακόμα πιο δυνατό. Αλλά κράτα πάντα στο μυαλό σου: το Middleware δεν είναι η μοναδική σου γραμμή άμυνας. Defense-in-depth, επαλήθευση σε κάθε layer, και γρήγορος, minimal κώδικας στο middleware — αυτά είναι τα κλειδιά.
Σε συνδυασμό με Server Actions για mutations και Streaming/Suspense/PPR για rendering, το Middleware ολοκληρώνει την τριάδα εργαλείων που χρειάζεσαι για σύγχρονες, ασφαλείς και γρήγορες Next.js εφαρμογές.