Proxy w Next.js zwykle zaczyna się niewinnie. Najpierw dopisujesz jedno przekierowanie ze starej ścieżki. Potem wyjątek dla draft mode. Później obsługę języka, nagłówki bezpieczeństwa, blokadę panelu admina, kilka historycznych adresów URL i warunek dla bota indeksującego.
Po kilku miesiącach masz jeden plik, którego nikt nie chce dotykać. Każda zmiana wygląda jak ryzyko: czy przekierowanie wykona się przed wykryciem języka? Czy preview z Sanity nadal zadziała? Czy ochrona prywatnej sekcji nie złapie statycznych assetów? Czy nowy redirect nie stworzy pętli?
Ten artykuł pokazuje praktyczny sposób wyjścia z takiego stanu. Nie chodzi o samą zmianę biblioteki ani o modne przepisywanie kodu. Chodzi o to, żeby routing brzegowy stał się czytelny, testowalny i odporny na dokładanie kolejnych wyjątków. NEMO pomaga potraktować Proxy jak zestaw jawnych reguł proxy, a nie jako jeden coraz większy warunek.
Proxy, który rósł szybciej niż aplikacja
Typowy problem wygląda tak: w jednym pliku miesza się kilka odpowiedzialności, które powinny być rozumiane osobno. Przekierowania SEO żyją obok autoryzacji. Wykrywanie języka żyje obok draft mode. Nagłówki bezpieczeństwa żyją obok historycznych ścieżek po migracji bloga.
export function Proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/_next")) {
return NextResponse.next();
}
if (pathname.startsWith("/old-blog")) {
return NextResponse.redirect(new URL("/blog", request.url));
}
if (!pathname.startsWith("/pl") && prefersPolish(request)) {
return NextResponse.redirect(new URL(`/pl${pathname}`, request.url));
}
if (pathname.startsWith("/dashboard") && !hasSession(request)) {
return NextResponse.redirect(new URL("/login", request.url));
}
const response = NextResponse.next();
response.headers.set("x-frame-options", "DENY");
return response;
}export function Proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/_next")) {
return NextResponse.next();
}
if (pathname.startsWith("/old-blog")) {
return NextResponse.redirect(new URL("/blog", request.url));
}
if (!pathname.startsWith("/pl") && prefersPolish(request)) {
return NextResponse.redirect(new URL(`/pl${pathname}`, request.url));
}
if (pathname.startsWith("/dashboard") && !hasSession(request)) {
return NextResponse.redirect(new URL("/login", request.url));
}
const response = NextResponse.next();
response.headers.set("x-frame-options", "DENY");
return response;
}Kod może działać, ale trudno go bezpiecznie rozwijać. Największym problemem nie jest liczba linii, tylko brak jawnego modelu: które reguły mają pierwszeństwo, które są wyjątkami, które tylko dekorują odpowiedź, a które kończą obsługę żądania.
Najpierw nazwij odpowiedzialności
Zanim użyjesz NEMO, warto rozdzielić problem na odpowiedzialności. W większości aplikacji pojawia się podobna lista:
ścieżki techniczne, których Proxy nie powinien dotykać,
historyczne przekierowania po migracjach,
kanonikalizacja adresów URL,
obsługa języka i prefiksów locale,
wyjątki dla draft mode i preview,
ochrona sekcji wymagających sesji,
nagłówki bezpieczeństwa,
metryki i diagnostyka żądań.
Dopiero gdy ta lista jest jawna, można ustalić kolejność. To jest ważniejsze niż sam wybór biblioteki. Dobra kolejność usuwa większość przypadkowych regresji.
Reguła powinna robić jedną rzecz
Najprostszy model to funkcja, która przyjmuje żądanie i opcjonalnie zwraca odpowiedź. Jeśli nic nie zwraca, następna reguła może działać dalej.
type ProxyRule = (request: NextRequest) => NextResponse | undefined;
const rules: ProxyRule[] = [
skipInternalPaths,
legacyRedirects,
draftModeBypass,
localeRedirects,
authGate,
securityHeaders,
];
export function Proxy(request: NextRequest) {
for (const rule of rules) {
const response = rule(request);
if (response) {
return response;
}
}
return NextResponse.next();
}type ProxyRule = (request: NextRequest) => NextResponse | undefined;
const rules: ProxyRule[] = [
skipInternalPaths,
legacyRedirects,
draftModeBypass,
localeRedirects,
authGate,
securityHeaders,
];
export function Proxy(request: NextRequest) {
for (const rule of rules) {
const response = rule(request);
if (response) {
return response;
}
}
return NextResponse.next();
}NEMO pozwala zamienić taki wzorzec w bardziej uporządkowaną kompozycję proxy. Najważniejsza zasada pozostaje taka sama: jedna reguła, jedna odpowiedzialność, jawna kolejność.
Wyjątki techniczne na samym początku
Proxy nie powinien przypadkowo obsługiwać plików Next.js, obrazów, favicony, healthchecków ani endpointów, które muszą pozostać neutralne. Te wyjątki warto trzymać na początku, bo oszczędzają czas i zmniejszają ryzyko ubocznych efektów.
const INTERNAL_PREFIXES = ["/_next", "/favicon.ico", "/robots.txt", "/sitemap.xml"];
function skipInternalPaths(request: NextRequest) {
const { pathname } = request.nextUrl;
if (INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next();
}
}const INTERNAL_PREFIXES = ["/_next", "/favicon.ico", "/robots.txt", "/sitemap.xml"];
function skipInternalPaths(request: NextRequest) {
const { pathname } = request.nextUrl;
if (INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next();
}
}To wygląda banalnie, ale takie warunki często trafiają losowo w środek pliku. Wtedy nowa reguła może niechcący zacząć przekierowywać assety albo psuć endpoint preview.
Draft mode jako osobny kontrakt
Draft mode jest szczególnie podatny na regresje, bo działa inaczej niż normalny ruch. Preview potrafi widzieć wersje robocze, nieopublikowane slugi i parametry techniczne. Nie chcesz, żeby reguła językowa albo SEO redirect wyprzedziły logikę preview.
function draftModeBypass(request: NextRequest) {
if (request.nextUrl.searchParams.has("__vercel_draft")) {
return NextResponse.next();
}
if (request.cookies.has("__prerender_bypass")) {
return NextResponse.next();
}
}function draftModeBypass(request: NextRequest) {
if (request.nextUrl.searchParams.has("__vercel_draft")) {
return NextResponse.next();
}
if (request.cookies.has("__prerender_bypass")) {
return NextResponse.next();
}
}Nie oznacza to, że draft mode ma omijać wszystkie zabezpieczenia w każdej aplikacji. Oznacza to, że jego zasady powinny być nazwane, przetestowane i umieszczone w kolejności świadomie.
Przekierowania SEO bez pętli
Przekierowania historyczne najlepiej opisać jako dane, a nie jako serię warunków. Dzięki temu łatwiej je sprawdzić, usunąć po czasie i testować tabelarycznie.
const redirects = new Map([
["/old-blog/docker", "/blog/docker-for-beginners-practical-guide"],
["/pl/old-blog/docker", "/pl/blog/docker-dla-poczatkujacych"],
]);
function legacyRedirects(request: NextRequest) {
const target = redirects.get(request.nextUrl.pathname);
if (!target) {
return;
}
return NextResponse.redirect(new URL(target, request.url), 308);
}const redirects = new Map([
["/old-blog/docker", "/blog/docker-for-beginners-practical-guide"],
["/pl/old-blog/docker", "/pl/blog/docker-dla-poczatkujacych"],
]);
function legacyRedirects(request: NextRequest) {
const target = redirects.get(request.nextUrl.pathname);
if (!target) {
return;
}
return NextResponse.redirect(new URL(target, request.url), 308);
}Przy większej liczbie reguł dodaj walidację, która wykrywa redirect na tę samą ścieżkę. Pętla w Proxy potrafi zablokować stronę zanim aplikacja w ogóle wyrenderuje błąd.
Locale routing: nie mieszaj języka z treścią
Reguła językowa powinna decydować o ścieżce, nie o tym, czy dany artykuł istnieje w konkretnym języku. To są dwa różne problemy. Proxy może dopilnować prefiksu /pl, ale strona bloga nadal musi sprawdzić, czy dokument ma slug.pl albo slug.en.
function localeRedirects(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/pl")) {
return;
}
if (prefersPolish(request)) {
const url = request.nextUrl.clone();
url.pathname = `/pl${pathname}`;
return NextResponse.redirect(url, 307);
}
}function localeRedirects(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/pl")) {
return;
}
if (prefersPolish(request)) {
const url = request.nextUrl.clone();
url.pathname = `/pl${pathname}`;
return NextResponse.redirect(url, 307);
}
}To rozdzielenie jest istotne dla draftów. Brak angielskiego sluga nie powinien powodować renderowania polskiego artykułu pod angielską ścieżką. To zadanie query i strony szczegółowej, nie Proxy.
Testuj adresy, a nie implementację
Najlepsze testy Proxy mówią językiem URL-i. Nie sprawdzaj, czy funkcja wewnętrzna została wywołana. Sprawdź, co zobaczy użytkownik, crawler albo Sanity preview.
it.each([
["/old-blog/docker", 308, "/blog/docker-for-beginners-practical-guide"],
["/pl/blog/docker-dla-poczatkujacych", 200, null],
["/blog/post?__vercel_draft=1", 200, null],
["/_next/static/chunk.js", 200, null],
])("handles %s", (path, status, location) => {
const response = Proxy(makeRequest(path));
expect(response.status).toBe(status);
expect(response.headers.get("location")).toBe(location);
});it.each([
["/old-blog/docker", 308, "/blog/docker-for-beginners-practical-guide"],
["/pl/blog/docker-dla-poczatkujacych", 200, null],
["/blog/post?__vercel_draft=1", 200, null],
["/_next/static/chunk.js", 200, null],
])("handles %s", (path, status, location) => {
const response = Proxy(makeRequest(path));
expect(response.status).toBe(status);
expect(response.headers.get("location")).toBe(location);
});Warto mieć osobne przypadki dla ruchu produkcyjnego i draft mode. To właśnie tam najczęściej wychodzą konflikty między językiem, preview i przekierowaniami.
Migracja bez wielkiego wybuchu
Nie przepisuj całego Proxy w jednym commicie. Bezpieczna migracja wygląda nudno:
Zapisz aktualne zachowanie w testach tabelarycznych.
Wydziel jedną odpowiedzialność do reguły proxy.
Zachowaj kolejność reguł w jednym miejscu.
Uruchom testy po każdym kroku.
Dopiero wtedy usuń stary warunek z dużego pliku proxy.
Jeżeli podczas wydzielania reguły musisz przekazać pięć parametrów i znać szczegóły trzech innych reguł, to znak, że granica odpowiedzialności jest zła.
Co zyskujesz po uporządkowaniu
Dobrze zaprojektowany Proxy nie musi być krótki. Ma być przewidywalny. Po refaktorze powinieneś umieć odpowiedzieć na pytania:
która reguła działa pierwsza,
która reguła kończy obsługę żądania,
które ścieżki są wyłączone z Proxy,
jak draft mode omija normalne przekierowania,
gdzie dopisać nowy redirect bez naruszania auth i i18n.
To jest prawdziwy sens użycia NEMO: mniej ukrytej logiki, mniej przypadkowych pętli i mniej stresu przy każdej zmianie routingu.