Next.js route handler som proxy for /autocomplete
Hold API-nøglen på serveren med en Next.js App Router route handler, der proxyer /autocomplete med cache-headers og sender rate-limit-headers videre.
En proxy er et lille endpoint på din egen server. Browseren kalder det uden nøgle, og serveren kalder https://api.danadresse.dk med nøglen fra en miljøvariabel. Nøglen kommer aldrig ud i browseren, og du bestemmer selv, hvilke parametre der må sendes videre.
Det er alternativet til en domænelåst browsernøgle (se Browsernøgler og tilladte domæner). Vælg proxyen, når du ikke vil have en nøgle i sidens kildekode, når du vil cache svar på din egen CDN, eller når du senere vil kalde endpoints, som browsernøgler ikke må bruge.
1. Læg nøglen i en servervariabel
Brug en almindelig nøgle uden tilladte domæner. Variablen må ikke have præfikset NEXT_PUBLIC_, for så bliver den skrevet ind i klientbundtet.
# .env.local (kun på serveren)
DANADRESSE_API_KEY=dawa_live_din_servernøgle2. Skriv route handleren
Filen app/api/adresse/route.ts svarer på GET /api/adresse?q=…. Den gør fire ting:
- Sender kun en fast liste af parametre videre. Uden den liste kunne enhver bruge din proxy til at kalde andre endpoints på din nøgle.
- Begrænser
per_sideog længden afq. - Sætter
Cache-Control, så din CDN kan genbruge svaret. Svaret afhænger kun af parametrene, ikke af hvem der spørger, så det er sikkert at cache offentligt hos dig. API'et selv senderprivate, max-age=300, fordi svaret der er knyttet til en nøgle. - Sender rate-limit-headers videre, så klienten kan se, hvornår den skal vente.
// app/api/adresse/route.ts import type { NextRequest } from "next/server"; const UPSTREAM = "https://api.danadresse.dk/autocomplete"; const TILLADTE_PARAMETRE = ["q", "type", "postnr", "kommunekode", "adgangsadresseid", "caretpos", "fuzzy", "srid"]; const VIDERESEND = [ "x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset", "x-ratelimit-minute-limit", "x-ratelimit-minute-remaining", "x-ratelimit-minute-reset", "retry-after", "x-quota-grace", "x-quota-remaining", ]; export async function GET(req: NextRequest) { const ind = req.nextUrl.searchParams; const q = (ind.get("q") ?? "").trim(); if (q.length < 2 || q.length > 120) { return Response.json([], { headers: { "Cache-Control": "public, max-age=60" } }); } const url = new URL(UPSTREAM); for (const navn of TILLADTE_PARAMETRE) { const v = ind.get(navn); if (v !== null) url.searchParams.set(navn, v); } const perSide = Math.min(Math.max(Number(ind.get("per_side")) || 8, 1), 20); url.searchParams.set("per_side", String(perSide)); let res: Response; try { res = await fetch(url, { headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY! }, cache: "no-store", signal: AbortSignal.timeout(5000), }); } catch { return Response.json( { error: "upstream_unavailable" }, { status: 503, headers: { "Retry-After": "5", "Cache-Control": "no-store" } }, ); } // 401/403 betyder, at din servernøgle er forkert eller spærret. Det er din fejl, // ikke brugerens: log den og svar 502 uden at vise API'ets fejltekst. if (res.status === 401 || res.status === 403) { console.error("Danadresse afviste servernøglen:", res.status, await res.text()); return Response.json({ error: "bad_gateway" }, { status: 502, headers: { "Cache-Control": "no-store" } }); } const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" }); for (const h of VIDERESEND) { const v = res.headers.get(h); if (v) headers.set(h, v); } headers.set( "Cache-Control", res.ok ? "public, max-age=60, s-maxage=300, stale-while-revalidate=600" : "no-store", ); return new Response(res.body, { status: res.status, headers }); }
Route handleren læser req.nextUrl, så Next.js behandler den som dynamisk og cacher den ikke selv. Cachen ligger i Cache-Control-headeren, som din CDN (fx Vercel) respekterer.
3. Kald proxyen fra klienten
Klienten bruger samme kode som i Adressefelt i React. Skift URL'en, og fjern headeren:
const url = new URL("/api/adresse", window.location.origin); url.searchParams.set("q", q); url.searchParams.set("type", "adresse"); const res = await fetch(url, { signal: ctrl.signal }); if (res.status === 429) { const vent = Number(res.headers.get("Retry-After") ?? 5); // vis "prøv igen om lidt" og vent mindst `vent` sekunder }
Proxyen sender Retry-After, X-RateLimit-*-headerne og X-Quota-Grace/X-Quota-Remaining videre, så klienten læser dem præcis som ved et direkte kald til API'et.
4. Sæt din egen grænse pr. besøgende
Med en proxy deler alle besøgende én nøgle. Kontoens minutgrænse gælder derfor for hele dit site samlet, og den grænse pr. besøgende, som browsernøgler har, gælder ikke. Ét script, der hamrer løs på /api/adresse, kan bruge hele din kvote.
Sæt derfor en grænse pr. IP i din proxy. På Vercel kan du bruge Middleware med et Redis-baseret tællerbibliotek. I en almindelig Node-server er en tæller i hukommelsen nok, så længe du kører én proces. Se Express-eksemplet nedenfor.
Bemærk også, at CDN-cachede svar har de X-RateLimit-*-tal, der gjaldt, da svaret blev hentet. Brug dem som et fingerpeg, ikke som sandhed.
5. Samme proxy i Express
Har du ikke Next.js, gør en Express-server det samme. Eksemplet kræver Node 18 eller nyere (indbygget fetch) og npm install express:
import express from "express"; const app = express(); app.set("trust proxy", 1); // læs klientens IP fra X-Forwarded-For bag din load balancer const UPSTREAM = "https://api.danadresse.dk/autocomplete"; const TILLADTE_PARAMETRE = ["q", "type", "postnr", "kommunekode", "adgangsadresseid", "caretpos", "fuzzy", "srid"]; const VIDERESEND = [ "x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset", "x-ratelimit-minute-limit", "x-ratelimit-minute-remaining", "x-ratelimit-minute-reset", "retry-after", "x-quota-grace", "x-quota-remaining", ]; // Enkel tæller pr. IP i hukommelsen (kun til én proces). const VINDUE_MS = 60_000; const MAKS_PR_VINDUE = 60; const taeller = new Map(); setInterval(() => taeller.clear(), VINDUE_MS).unref(); function graensePrIp(req, res, next) { const n = (taeller.get(req.ip) ?? 0) + 1; taeller.set(req.ip, n); if (n > MAKS_PR_VINDUE) { res.set("Retry-After", "10").status(429).json({ error: "too_many_requests" }); return; } next(); } app.get("/api/adresse", graensePrIp, async (req, res) => { const q = String(req.query.q ?? "").trim(); if (q.length < 2 || q.length > 120) { res.set("Cache-Control", "public, max-age=60").json([]); return; } const url = new URL(UPSTREAM); for (const navn of TILLADTE_PARAMETRE) { if (typeof req.query[navn] === "string") url.searchParams.set(navn, req.query[navn]); } const perSide = Math.min(Math.max(Number(req.query.per_side) || 8, 1), 20); url.searchParams.set("per_side", String(perSide)); try { const upstream = await fetch(url, { headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY }, signal: AbortSignal.timeout(5000), }); if (upstream.status === 401 || upstream.status === 403) { console.error("Danadresse afviste servernøglen:", upstream.status, await upstream.text()); res.set("Cache-Control", "no-store").status(502).json({ error: "bad_gateway" }); return; } for (const h of VIDERESEND) { const v = upstream.headers.get(h); if (v) res.set(h, v); } res.set("Cache-Control", upstream.ok ? "public, max-age=60, s-maxage=300" : "no-store"); res.status(upstream.status).type("application/json").send(await upstream.text()); } catch { res.set({ "Retry-After": "5", "Cache-Control": "no-store" }).status(503).json({ error: "upstream_unavailable" }); } }); app.listen(process.env.PORT ?? 3000);
6. Se de headers, proxyen sender videre
Kald API'et direkte fra Node og se de headers, din proxy skal videresende. X-RateLimit-Reset er et Unix-tidsstempel for månedens nulstilling, og X-RateLimit-Minute-Reset er tidspunktet, hvor minutvinduet starter forfra:
const url = new URL("https://api.danadresse.dk/autocomplete"); url.searchParams.set("q", "Strandvejen 100"); url.searchParams.set("per_side", "3"); const res = await fetch(url, { headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY }, }); console.log("status:", res.status); for (const [navn, vaerdi] of res.headers) { if (navn.startsWith("x-ratelimit") || navn === "cache-control") { console.log(`${navn}: ${vaerdi}`); } }
Svaret ser nogenlunde sådan ud (tallene afhænger af din plan):
status: 200 cache-control: private, max-age=300 x-ratelimit-limit: 2000 x-ratelimit-minute-limit: 360 x-ratelimit-minute-remaining: … x-ratelimit-minute-reset: 1792065660 x-ratelimit-remaining: … x-ratelimit-reset: 1793577600
På Enterprise er X-RateLimit-Minute-Limit sat til unlimited, og der er ingen Minute-Remaining eller Minute-Reset. En gratiskonto, der har brugt sin månedskvote, får stadig svar op til 20 % mere; de svar har X-Quota-Grace: true og X-Quota-Remaining (kald tilbage i bufferen). Sådan håndterer du en 429 korrekt: Grænser, kvoter og 429.
Næste skridt
- Grænser, kvoter og 429
- Caching — hvad du må cache, og hvor længe.
- Browsernøgler og tilladte domæner — alternativet uden server.
- Oversigt: /nextjs-adresse-api.
Relaterede guides
Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)