Een web backlink-checker in Next.js, sleutel op de server bewaard
CORS is open, dus een browser kan de API direct aanroepen. Dit doen met een betaalsleutel is een fout. Deze pagina toont de dunne serverlaag die dit oplost.

Mythe versus de werkelijkheid
| Mythe | Werkelijkheid |
|---|---|
| "Open CORS betekent dat ik de sleutel in mijn React-bundel kan meesturen." | Open CORS betekent dat de call werkt. Iedereen kan de sleutel nog steeds lezen via de netwerktab en je saldo leegspenden. |
| "Een tabel met links moet op de client renderen omdat het een dashboard is." | Fetch op de server en stuur HTML. Je gebruiker ziet rijen bij de eerste paint en je sleutel verlaat de box nooit. |
| "Gratis acties hebben ook bescherming nodig." | domain, overview, stats en suggest hebben geen sleutel nodig, dus een browser mag ze direct aanroepen. |
| "Ik moet bij elke paginaweergave opnieuw fetchen." | De index wordt ongeveer maandelijks herbouwd. Een cache van 24 uur verliest niets. |
| "De proxy moet doorsturen wat de client ook stuurt." | Stel een allow-list in voor de acties die je gebruikt. Een doorgestuurde action=search kost 3 credits. |
De route-handler
App Router, bestand app/api/links/route.ts. De omgevingsvariabele bereikt nooit de client-bundel omdat deze geen NEXT_PUBLIC_ prefix heeft.
import { NextRequest, NextResponse } from "next/server";
const ALLOWED = new Set(["refdomains", "anchors", "toppages"]);
export async function GET(req: NextRequest) {
const p = req.nextUrl.searchParams;
const action = p.get("action") ?? "";
const q = (p.get("q") ?? "").toLowerCase();
if (!ALLOWED.has(action) || !/^[a-z0-9.-]+$/.test(q)) {
return NextResponse.json({ ok: false, error: "bad request" }, { status: 400 });
}
const url = `https://seomasters.dev/api.php?action=${action}&q=${q}&per=100`;
const upstream = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SEO_API_KEY}` },
next: { revalidate: 86400 },
});
if (upstream.status === 402) {
return NextResponse.json({ ok: false, error: "data temporarily unavailable" }, { status: 503 });
}
return new NextResponse(upstream.body, { status: upstream.status, headers: { "content-type": "application/json" } });
}Waarom de handler er zo uitziet
Allow-list van acties
Een bug in je UI mag niet per ongeluk een 3-credit search-call triggeren.
Regex op q
Alleen een kale hostname komt door. Geen scheme, geen pad, geen query string die upstream gesmokkeld wordt.
revalidate: 86400
Eén upstream-call per domein per dag. Met maandelijkse data is een langere periode ook prima.
402 gemapt naar 503
Je bezoekers hoeven niet te weten dat je saldo leeg is. Jij wel, dus log het.

Rendering: wat een crawler en een klant beiden zien
Als dit dashboard achter een login zit, is server- of client-side rendering een UX-keuze. Als het publiek is, zoals een gratis lookup-pagina die je wilt laten indexeren, render dan de eerste tabel op de server.
Een pagina die als een lege schil aankomt en zich vult na hydration is een risico voor indexering en voor langzame telefoons. Google kan JavaScript uitvoeren, maar het plakt die stap in de wachtrij en komt er niet altijd snel aan toe.
Houd de gratis overview op de server
Zet de goedkope, gratis overview-call in een Server Component en laat de bezoeker de betaalde, diepere pulls triggeren via een knop. Deze split houdt je credit-uitgave ook voorspelbaar.
Gratis calls kosten niets, dus een server-gerenderde overview voegt geen credit-uitgave toe. Alleen de betaalde pulls hebben een knop en een sessie nodig.
Denk eerst na over de lege staat. Een bezoeker die op een lege tabel landt, denkt dat de tool kapot is, en een snelle tweede paint maakt die eerste indruk niet ongedaan.
Kies de gratis acties voor het eerste scherm en de betaalde voor drill-downs. Een bezoeker die een pagina opent en een gratis samenvatting ziet, heeft geen reden om per ongeluk op een betaalde knop te drukken.
Link de uitleg, niet alleen het nummer
De cijfers koppelen is het makkelijke deel. Ze uitleggen is moeilijker, dus link de DR-formule vanuit de tooltip en vermeld dat de index maandelijks ververst.
Een bezoeker die een score ziet zonder de formule, raadt er eentje, dus de link hoort naast het nummer op het scherm.

Server- en clientonderdelen van één dashboard
De meeste dashboards hebben beide soorten code nodig. Eigenaarschap bepaalt de split. De sleutel, de data en de toegangsregels hebben elk één thuis. Een web backlink-checker pagina die publiek is, heeft dezelfde split nodig als een achter een login.
Fetch op de server, sorteren op de client
Een backlink-tabel is een redelijk client-component als mensen erin sorteren, filteren en erdoorheen bladeren. De rijen moeten van een server-component komen die je route al heeft aangeroepen, zodat de browser de sleutel of de upstream-URL nooit vasthoudt.
Tien rijen sorteren is direct op de client. Vijfduizend rijen sorteren in een nieuw verzoek elke keer is langzaam, dus houd de volledige set in het component dat je al hebt geladen.
Houd het simpel. Een tabel die direct sorteert, wint het van een die bij elke klik opnieuw fetcht.
Houd de client-grens laag
Duw de grens zo ver naar beneden in de boom als je kunt. Een dynamisch geïmporteerd, client-only widget render in de browser, terwijl het component dat de rijen heeft opgehaald op de server blijft. Geef rijen door als props en houd de fetch boven de grens.
Als je de fetch onder de grens verplaatst, maakt de browser de call zelf. Dat is de fout die de sectie over sleutellekken waarschuwt, dus controleer de Network-tab na elke wijziging aan deze grens.
Waar precies valt de lijn? Teken hem eerst op papier, met één vak voor de fetch en één voor elke interactieve controle, en tel de pijlen die erdoorheen gaan.
Shell en sessiecontroles blijven op de server
Layout en sessiecontroles bepalen wat een bezoeker überhaupt mag zien, dus ze horen op de server. Interactieve tabellen, formulieren en grafieken kunnen daarna hydrateren zonder die beslissing te veranderen.
Een client-side guard kan een knop verbergen, maar een verzoek niet stoppen. De route-handler en de server-layout zijn waar toegang daadwerkelijk wordt afgedwongen.
Stuur dan hetzelfde verzoek zonder de cookie en bevestig dat de server het weigert, niet alleen de pagina.
Dezelfde regel geldt voor de proxy zelf. Een sessiecontrole die alleen in de browser gebeurt, kan de route niet beschermen, dus de route controleert de sessie ook voordat hij een credit uitgeeft.
Cache per domein, niet per bezoeker
De route bouwt zijn upstream-URL van het domein, dus de cache-key volgt het domein ook. Eén bezoeker die een site opzoekt, warmt de pagina op voor de volgende bezoeker die over dezelfde site vraagt.
Dat is het punt van de dagelijkse revalidate in de handler. Twee bezoekers op hetzelfde domein kosten één upstream-call, en duizend bezoekers op duizend domeinen kosten duizend calls, dus de cache is het budget.
Let ook op het omgekeerde geval. Een pagina die zijn query string varieert met een timestamp, breekt de cache volledig, en de rekening toont het voordat de logs dat doen.
Revalidate op een schema dat past bij de index. Een dagelijks venster past bij maandelijkse data, en een kortere voegt alleen calls toe zonder nieuwe feiten.
Verharding, in de volgorde waarin we het zouden doen
De handler is het makkelijke deel. De verharding is waar een sleutel lekt of een saldo leegloopt, dus werk deze volgorde door.
Scan je eigen Network-tab
Open Chrome DevTools op je eigen dashboard en lees de Network-tab. Kijk goed. Als een verzoek daar sk_live_ ergens laat zien, draai de sleutel vandaag nog om. Die check duurt een minuut en vindt de meest voorkomende lek.
Zoek ook in de response bodies en niet alleen in de request headers. Een sleutel die in een foutmelding wordt weergegeven, is nog steeds een lek.
Limiteer calls per gebruiker
Volgende stap: voeg een limiet per gebruiker toe. Zet het voor de route. De handler hierboven geeft je credit uit voor iedereen die erbij kan, dus koppel het aan een sessie en limiteer calls per uur. Kies de limiet uit de budget-mathematica op de credit-pagina.
Begin genereus en maak het later strakker. Een limiet die echte gebruikers op dag één blokkeert, leert hen een andere tool te zoeken.
Noem wie bijbetaalt
Ook het vermelden waard een regel in het runbook: wie wordt gebeld als het saldo onder een bepaald niveau daalt, en wie betaalt bij. Noem de persoon.
Schrijf de bijbetaalstap op als een checklist met de link naar de accountpagina en de verwachte doorlooptijd. Een runbook dat alleen in het hoofd van één persoon bestaat, faalt de eerste keer dat die persoon met verlof is. De stap zelf duurt minuten, dus het opschrijven is het grotere werk.
Log het saldo voordat het op raakt
Log vervolgens _credits van elke upstream-response naar je eigen metrics. Een grafiek van het resterende saldo, dagelijks gesampled, waarschuwt je een week voordat HTTP 402 komt.
Kies het alertniveau uit je eigen wekelijkse gebruik. Een nummer gekozen uit een template past zelden bij je echte calls.
Houd de laatste waarde die je zag ook in het geheugen en in de log, zodat de route betaalde pulls kan weigeren zodra het nummer onder het alertniveau daalt. Die check is één regel, en het stopt een burst van verkeer van het uitgeven van de rest.
Houd de allow-list op één plek
Een andere gewoonte die zich uitbetaalt. Houd de allow-list in een gedeelde constante, geïmporteerd door de handler en door de tests, zodat een nieuwe actie niet zonder dat iemand een zichtbare lijst bewerkt en een reviewer het opmerkt, de productie kan bereiken.
Eén bestand, één import. Iedereen moet niet hoeven zoeken naar de lijst in drie componenten om te weten wat de proxy accepteert.
Test de runtime waar je naar deployt
Tot slot, draai de handler onder Node 20 of de edge runtime van je keuze en test beide. De upstream-body streamen, zoals de code dat doet, werkt in elk, maar header-handling verschilt en is het testen waard.
Voeg één test toe die de route aanroept met een slechte actie en één met een geldige actie, en assert op de statuscodes. Die twee tests vangen de meeste regressies in de allow-list en de header-handling.
De proxy in de juiste volgorde inbedden
- 1
Voeg de sleutel op de server toe
Sla de sleutel op in je server-omgeving onder een naam zonder een publiek prefix, en herstart dan de dev-server zodat deze wordt geladen.
- 2
Schrijf eerst de allow-list
Lijst de acties op die je schermen echt aanroepen. Alles anders krijgt een 400 van je eigen route, voordat het de upstream API bereikt.
- 3
Fetch je route vanuit een server-component
Render de rijen waar de data aankomt. De browser roept alleen je eigen domein aan, dus het upstream-adres blijft buiten de pagina.
- 4
Houd het saldo in de gaten
Log
_creditsop elke upstream-response en stel een alertniveau in waarop je actie kunt ondernemen, voordat een 402 een bezoeker bereikt als een 503.