hranici si nakreslete hned
Většina týmů se u edge runtimů dostane do problémů ze stejného důvodu jako u microservices, začnou řešit tvar deploymentu místo toho, jaké budou failure modes. Next.js 15 vám hodně usnadní nacpat logiku do middleware, route handlerů, server components a edge funkcí, až appka v demu působí rychle a za půl roku se v ní nedá vyznat. Pak se někdo zeptá, kde vlastně žijí entitlement checks, proč se výpočet daně pouští dvakrát a proč cache key závisí na hodnotě, kterou vidí jen browser.
Moje pravidlo je schválně tvrdé, protože měkká pravidla se ignorují. Edge kód existuje proto, aby rozhodl, jak se má request doručit. Go služby existují proto, aby rozhodly, co systém znamená. Pokud kus logiky mění význam objednávky, předplatného, schválení, faktury, claimu, reportu nebo uživatelské capability, patří do Go. Pokud rozhoduje o tom, jakou variantu renderovat, na který upstream sáhnout, jestli je signed cookie dost validní na to, aby request pokračoval, nebo jak přidat cache metadata a headers, edge je v pohodě.
Tahle hranice vydrží i pod tlakem. Next.js edge funkce může levně rozparsovat signed session cookie, odfiltrovat očividně neplatný provoz, přidat x-request-id, spočítat hrubý feature bucket a rozhodnout, jestli se má streamovat dashboard shell, nebo udělat redirect na /login. Ve chvíli, kdy ta samá funkce potřebuje permission expansion nad databází, počítání peněz, transakční zápisy, webhook secrets, retry semantiku nebo call, který ve špatný den trvá 800 ms, držíte v ruce špatný nástroj.
Ve steezr jsme tenhle split použili v customer portálech i interních systémech, kde webová vrstva musela působit okamžitě, zatímco skutečná doménová práce běžela za Go API a workery. Týmy, které drží edge tenký, shipují rychleji. Týmy, které nechají doménovou logiku protéct nahoru, tráví čas diffováním chování mezi middleware a backend handlery. To je přesně ten typ problému, který si způsobíte sami a stejně se pořád prodává jako architektura.
co má vlastnit edge
Next.js 15 je výborný na tvarování requestů. Držte ho tam. Edge vrstva by měla vlastnit čtyři věci: skládání response, kontroly přítomnosti session, segmentaci cache a personalizaci blízko uživatele, která snese stárnutí nebo hrubší granularitu.
Skládání response znamená streaming SSR, výběr routy, redirecty, rewrites, částečné skládání stránky a rozhodnutí, které backend endpointy zavolat pro first paint. Request na dashboard může paralelně fanoutnout do /api/me/summary a /api/org/current, hned vyrenderovat shell a pomalejší panely dostreamovat později přes React suspense. Tahle práce patří blízko requestu, protože zlepšuje vnímanou latenci, aniž by duplikovala doménová rozhodnutí.
Session checky na edge mají zůstat schválně hloupé. Ověřte signed cookie, zkontrolujte expiraci, případně vytáhněte sub, org_hint, session_id, roles_hint, a to pošlete jako headers do Go až ve chvíli, kdy projde verifikace podpisu. Když cookie chybí nebo je rozbitá, redirect. Když je validní, pokračujte. Nerozšiřujte role z pěti zdrojů, neodvozujte billing state, nevyhodnocujte tam plan limity.
Jednoduchý middleware může vypadat takhle:
1import { NextRequest, NextResponse } from 'next/server'2import { jwtVerify } from 'jose'34const secret = new TextEncoder().encode(process.env.SESSION_PUBLIC_VERIFY_KEY)56export async function middleware(req: NextRequest) {7 const token = req.cookies.get('session')?.value8 const rid = crypto.randomUUID()910 if (!token) {11 const res = NextResponse.redirect(new URL('/login', req.url))12 res.headers.set('x-request-id', rid)13 return res14 }1516 try {17 const { payload } = await jwtVerify(token, secret, { algorithms: ['EdDSA'] })18 const headers = new Headers(req.headers)19 headers.set('x-request-id', rid)20 headers.set('x-session-sub', String(payload.sub))21 headers.set('x-session-id', String(payload.sid))22 headers.set('x-org-hint', String(payload.org_hint ?? ''))23 return NextResponse.next({ request: { headers } })24 } catch {25 const res = NextResponse.redirect(new URL('/login', req.url))26 res.cookies.delete('session')27 res.headers.set('x-request-id', rid)28 return res29 }30}3132export const config = {33 matcher: ['/app/:path*']34}
To stačí. Všimněte si, co tam není: žádný call do databáze, žádné feature vyhodnocení s deseti targeting pravidly, žádné secrets pro downstream systémy, žádné billing checky. Pokud tohle potřebujete, volejte Go.
co musí vlastnit Go
Go 1.22 služby by měly vlastnit každé rozhodnutí, které byste fakt nechtěli debugovat na dvou místech. Business pravidla, idempotenci, SQL transakce, externí integrace, joby nad queue, zpracování dokumentů, příjem webhooků, audit trail, permission expansion a každý secret, který vás může bolet, když uteče do špatného runtime.
Zní to samozřejmě, dokud někdo nedá plan enforcement do server componenty, protože tam bylo jednodušší schovat tlačítko. Pak mobilní klient trefí Go API přímo, pravidlo se nevynutí a najednou máte dvě interpretace produktu. Stejný příběh u entitlementů, slevových pravidel, approval thresholds, export limitů, KYC stavu nebo jakéhokoli workflow, které mění persistovaná data. Backend musí být jediná spustitelná specifikace.
Go je na to lepší z úplně nudných důvodů, a to je v infrastruktuře ta největší pochvala. Dlouho běžící procesy, stabilní chování paměti, přímočará concurrency, dobré p99 pod loadem, first-class SQL drivery, jednoduché workery, rozumný tracing a žádné dohady o tom, které Web API v jakém runtime zrovna chybí. Transakce v Go je pořád transakce, ne debata o tom, jestli váš runtime dneska umí otevřít TCP socket.
Standardní write path by měl vypadat takto: Next.js pošle intent, Go znovu autentizuje session context z důvěryhodných headers nebo z původního tokenu, načte autoritativní user state, zkontroluje doménové invarianty, provede transakci, emitne outbox event a vrátí kompaktní výsledek. Ve službě žádné UI-specific větvení. Žádné HTML ve službě taky ne, pokud zrovna cíleně nestavíte dedikovaný BFF endpoint.
Go handler pro vytvoření objednávky by měl působit až bolestně obyčejně:
1func (h *Handler) PlaceOrder(w http.ResponseWriter, r *http.Request) {2 ctx := r.Context()3 sess, err := auth.SessionFromRequest(r)4 if err != nil {5 http.Error(w, "unauthorized", http.StatusUnauthorized)6 return7 }89 var cmd PlaceOrderCommand10 if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {11 http.Error(w, "invalid json", http.StatusBadRequest)12 return13 }1415 out, err := h.App.PlaceOrder(ctx, sess.Subject, cmd)16 if err != nil {17 h.Errors.Write(w, err)18 return19 }2021 writeJSON(w, http.StatusCreated, out)22}
Nic fancy. To je dobře. Přesně tam se totiž začíná plížit duplicita.
auth bez rozjíždění
Nejrychlejší způsob, jak tuhle architekturu zničit, je nechat edge a backend hádat se o identitu. Jedna signed cookie, jeden issuer, jeden příběh kolem rotace klíčů, jedna autoritativní sada claims. Všechno ostatní je odvozené.
U systémů, kde chcete mít věci pod kontrolou, preferuju v cookie opaque session ID, podepřené Redisem nebo PostgreSQL, protože revokace je triviální a drift v claims zmizí. Zároveň beru i signed JWT session cookie, když záleží na latenci a claims jsou schválně malé. V obou případech edge validuje jen tolik, aby mohl rozhodnout o delivery. Go validuje kvůli autoritě. Berte edge validaci jako pomocnou věc, která se hodí pro tvarování provozu, ale jako finální security boundary je k ničemu.
U JWT cookies držte claims sparse: sub, sid, exp, iat, případně org_hint. Odolejte pokušení nacpat tam deset rolí a dvacet flagů. Každá změna role se pak změní na problém s refreshí cookie a potichu tím učíte produktové inženýry věřit zastaralým claims. Pokud potřebujete aktuální permissions, Go je načte.
Flow requestu, který doporučuju, je jednoduchý. Browser pošle session cookie do Next.js. Middleware ověří podpis a expiraci, nastaví x-request-id, při server-side fetchích přepošle původní cookie do Go a může přidat pár ověřených hint headers pro logy. Go ignoruje jakékoli nedůvěryhodné identity headers z veřejného internetu, znovu ověří cookie nebo rozresolveuje opaque session ID a teprve potom spočítá skutečný actor context. Pokud sedíte za privátní síťovou hranicí mezi Next.js a Go, podepisujte interní forwarding headers přes HMAC a rotujte ten klíč jako jakýkoli jiný secret.
Server-side fetch v Next.js route handleru nebo server componentě by měl být explicitní:
1const res = await fetch(`${process.env.API_ORIGIN}/v1/orders`, {2 method: 'POST',3 headers: {4 cookie: `session=${cookieStore.get('session')?.value ?? ''}`,5 'x-request-id': headers().get('x-request-id') ?? crypto.randomUUID(),6 'content-type': 'application/json'7 },8 body: JSON.stringify(payload),9 cache: 'no-store'10})
Pokud se tomu můžete vyhnout, neberte žádné magické auth SDK. Polovina z nich se po druhém custom požadavku změní v archeologické naleziště. Signed cookies, explicitní forwarding, backend revalidace, hotovo.
streaming a flags
Streaming SSR je místo, kde tenhle split začne opravdu vracet hodnotu. Next.js 15 umí poslat smysluplné UI za desítky milisekund, když přestanete čekat na každou backend závislost před renderem. Shell stránky, navigace, hlavní počty, základní stav účtu, tohle může odtéct hned. Pomalé widgety se můžou suspendnout, navázat později nebo se hydratovat client-side, pokud nejsou kritické.
Past je v tom, že z Next.js uděláte druhou orchestrační vrstvu s doménovou inteligencí. Kompozici stránek nechte v Next.js, semantiku dat v Go. Dashboard route může chtít /v1/me/home, /v1/org/billing-summary a /v1/announcements. Go rozhoduje, co který payload znamená. Next.js rozhoduje, jak ty payloady vyrenderovat a streamovat. Když je jeden panel pomalý, chcete fallback UI, ne 1,8 sekundy TTFB, protože server component čekala na všechno.
Feature flags potřebují stejnou disciplínu. Edge může vlastnit hrubé delivery flags, věci jako locale varianta, bucket marketingového experimentu, případně lehký layout treatment odvozený ze signed cookie nebo deterministického user hashe. Produktové flags, které ovlivňují permissions, pricing, workflow kroky, shape API nebo chování jobů, patří do Go, bez debat. Když flag mění business výsledek, vyhodnocuje ho služba.
Dobře funguje pattern se dvěma vrstvami flags, ale s oddělenými názvy a ownership. edge.nav_v2 žije v Next.js a můžete ho agresivně cachovat. domain.invoice_auto_submit žije v Go a vyhodnocuje se v handlerech, jobech i webhoocích. Nikdy nemirrorujte stejný flag na obou místech. Na diagramu to vypadá čistě, při rolloutu z toho vznikne chaos, protože jedna cache invaliduje později a support pak kouká na screenshoty, které nesedí se zápisy v databázi.
Kvůli latenci batchujte tam, kde to dává smysl. Dejte Next.js jeden backend endpoint šitý pro first paint, pokud stránka potřebuje pět malých readů a jeden drahý agregát. Malá BFF plocha v Go je v pohodě, pokud zůstane blízko prezentace a nenasákne do sebe React concerns. Cíl je míň hopů, ne další monolit.
observability přes hranici
Pokud nepřenášíte trace context přes hranici mezi Next.js a Go, budete z každého zpomalení obviňovat špatnou vrstvu. Browser waterfall říká jedno, Vercel nebo edge logy druhé, vaše Go traces ukazují hezký handler za 90 ms a někde mezi tím se ztratí 600 ms kvůli DNS, TLS reuse, backpressure na queue nebo nechtěně necachovanému fetchi v server componentě.
Používejte OpenTelemetry end to end. Next.js umí emitovat spans pro middleware, route handlery, server actions i server-side fetche. Go má vyspělou podporu OTel SDK a rozumně jednoduché exportery. Vyberte jeden formát trace headers, W3C trace context je jasná volba, propagujte traceparent a tracestate, přidejte x-request-id kvůli grepovatelným logům a sjednoťte service names. Když logy říkají request_id=abc123 trace_id=9f... user_id=42 org_id=7, debugování je inženýrská práce, ne folklor.
Užitečná konvence je tahle: edge vygeneruje request ID, pokud chybí, backend ho zachová a workery ho dědí přes metadata jobu. U async práce připojte původní trace ID jako link, místo abyste si namlouvali, že jde o stejný span tree. Dostanete tak graf, ve kterém se dá orientovat, bez lhaní sami sobě.
Metriky by měly tu hranici odrážet přímo. Sledujte redirect rate na edge, počet auth failů v middleware, SSR shell TTFB, backend first-byte latency, fan-out count na route, cache hit ratio u personalizovaného versus veřejného obsahu a počet renderů stránky, které blokoval backend call, bez kterého by se to obešlo. Jeden ošklivý, ale poctivý dashboard je lepší než třicet naleštěných, kterým stejně nikdo z inženýrů nevěří.
To samé platí pro chyby. Když Next.js chytí TypeError: fetch failed s cause: connect ETIMEDOUT 10.0.12.14:8080, pošlete do logů přesně tuhle hlášku spolu s route, upstreamem a request ID. Když Go vrátí pq: deadlock detected nebo context deadline exceeded, nebalte to do nějaké ozdobné třídy InternalServerErrorException, ze které zmizí všechno užitečné. Systémy se opravují pomocí konkrétních detailů.
recept na rozdělení odpovědností
Pokud rozřezáváte monolit na dvouvrstvý stack, nezačínejte tím, že odtrhnete náhodné endpointy. Začněte klasifikací kódu na delivery, doménu a async side effects, a přesouvejte to v tomhle pořadí.
Delivery jde nejdřív do Next.js. Middleware auth presence checks, redirecty, rewrites, response headers, cache keys, SSR kompozice, orchestrace fetchů na úrovni route, chování statických assetů, user-facing error boundaries. Doména zůstává v monolitu, dokud ji Go služba neumí převzít celou, včetně testů. Side effects jako e-maily, webhooky, exporty, OCR, generování reportů, document pipelines a sync joby přesouvejte za workery nad queue v Go až ve chvíli, kdy je stabilní doménová akce, která je emituje.
Praktická migrační sekvence pro typickou SaaS appku vypadá takto. Databázi chvíli nechte tam, kde je. Vedle monolitu postavte Go API s tenkým auth balíčkem a jedním read-only endpointem, který použije nová route v Next.js. Ještě před trafficem přidejte tracing. Jeden write path přesouvejte až ve chvíli, kdy umíte invariant vyjádřit jednou v Go a starou větev smazat. Background joby dejte za outbox tabulku, pokud na nich záleží. Odmítněte sdílené business logic balíčky mezi TypeScriptem a Go, vytváří fake konzistenci a reálnou bolest.
Checklist pro každý kus logiky je krátký. Sahá to na secrets nad rámec ověření session. Vyžaduje to transakci. Může to běžet déle než pár stovek milisekund. Musí to být identické napříč webem, mobilem, joby a webhooky. Povede zastaralá cache nebo zastaralý claim ke špatnému rozhodnutí. Potřebuje to přímý přístup do PostgreSQL, Redis streams, S3 multipart uploadů nebo privátní sítě. Pokud je odpověď na kteroukoli z těch otázek ano, patří to do Go.
Edge runtimy jsou skvělé na to, aby systém působil rychle. Jsou mizerné místo pro schovávání významu systému. Když tuhle hranici budete respektovat, stack zůstane srozumitelný. To je mnohem důležitější než nacpat do architektury ještě jeden box, který dobře vypadá na diagramu.
