En Node.js-klient med fetch og @danadresse/client
Skriv en Node-klient med fetch, timeouts, fejlkoder og Retry-After, eller brug npm-pakken @danadresse/client med et retry-lag.
Node 18 og nyere har fetch og AbortSignal.timeout indbygget, så en klient til Danadresse kræver ingen afhængigheder. Denne guide viser først et lille modul med fetch, der dækker autocomplete, opslag, datavask, reverse geokodning, fejlkoder og 429. Derefter viser den npm-pakken @danadresse/client og det, du selv skal lægge ovenpå.
Koden kører på serveren. Nøglen i DANADRESSE_API_KEY må ikke sendes til browseren. Skal browseren selv kalde API'et, så brug en domænelåst nøgle, se Browsernøgler og tilladte domæner.
Modulet med fetch
Gem filen som danadresse.mjs. Importerer du den, får du funktionerne. Kører du den direkte med node danadresse.mjs, laver den seks kald som demo:
// danadresse.mjs: en lille klient til Danadresse API med fetch (Node 18+) import { realpathSync } from "node:fs"; import { pathToFileURL } from "node:url"; const BASE_URL = process.env.DANADRESSE_BASE_URL ?? "https://api.danadresse.dk"; const API_KEY = process.env.DANADRESSE_API_KEY; const TIMEOUT_MS = 10_000; const MAX_RETRIES = 2; export class DanadresseError extends Error { constructor(status, code, message, retryAfter, details = null) { super(`HTTP ${status} ${code}: ${message}`); this.status = status; this.code = code; // stabil kode, fx "invalid_api_key" eller "rate_limit_per_minute" this.retryAfter = retryAfter; // sekunder, eller null this.details = details; // fx resets_at og upgrade_url ved quota_exceeded } } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function toError(res) { let body = {}; try { body = await res.json(); } catch { // ikke JSON (fx en proxy-fejlside) } // DAWA-stier: {type,title,details,code} /api/v1: {error:{code,message}} const err = body?.error && typeof body.error === "object" ? body.error : body ?? {}; const ra = res.headers.get("retry-after"); return new DanadresseError( res.status, err.code ?? null, err.title ?? err.message ?? res.statusText, ra && /^\d+$/.test(ra) ? Number(ra) : null, err.details ?? null, ); } export async function get(path, params = {}) { const url = new URL(path, BASE_URL); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v)); for (let attempt = 0; ; attempt++) { let res; try { res = await fetch(url, { headers: { "X-Api-Key": API_KEY, Accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS), }); } catch (e) { // timeout (TimeoutError) eller netværksfejl (TypeError) if (attempt >= MAX_RETRIES) throw e; await sleep(1000 * 2 ** attempt); continue; } if (res.ok) return res.json(); const err = await toError(res); // 429 med Retry-After (minutgrænse) og 503 kan prøves igen. quota_exceeded har ingen Retry-After. if ((res.status === 429 || res.status === 503) && err.retryAfter && attempt < MAX_RETRIES) { await sleep(Math.min(err.retryAfter, 30) * 1000); continue; } throw err; } } function fromDawa(d, ag) { const [lon, lat] = ag.adgangspunkt?.koordinater ?? [null, null]; return { id: d.id, betegnelse: d.adressebetegnelse, postnr: ag.postnummer?.nr ?? null, kommunekode: ag.kommune?.kode ?? null, lon, lat, }; } export const autocomplete = (q, perSide = 5) => get("/autocomplete", { q, per_side: perSide }); export async function adresse(id) { const d = await get(`/adresser/${encodeURIComponent(id)}`); return fromDawa(d, d.adgangsadresse); } export async function vask(betegnelse) { const svar = await get("/datavask/adresser", { betegnelse }); const bedste = svar.resultater?.[0]; // aktueladresse = den gældende version (afviger, hvis den fundne er nedlagt) const a = bedste?.aktueladresse ?? bedste?.adresse ?? null; return { kategori: svar.kategori, // "A" | "B" | "C", kun på topniveau kandidater: svar.resultater?.length ?? 0, sikker: (svar.kategori === "A" || svar.kategori === "B") && a !== null, afstand: bedste?.vaskeresultat?.afstand ?? null, // 0 = præcis match id: a?.id ?? null, tekst: a ? `${a.vejnavn} ${a.husnr}, ${a.postnr} ${a.postnrnavn}` : null, }; } export async function reverse(lon, lat) { const d = await get("/adgangsadresser/reverse", { x: lon, y: lat }); return fromDawa(d, d); } // Demo: køres kun, når filen startes direkte (node danadresse.mjs) if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) { for (const f of await autocomplete("Rådhuspladsen 1, 4200", 3)) console.log("forslag:", f.forslagstekst); const a = await adresse("0a3f50ae-b4ee-32b8-e044-0003ba298018"); console.log("opslag:", a.betegnelse, a.lon, a.lat); for (const tekst of ["Strandvejen 100, 2900 Hellerup", "Rentemestervej 8, 2. tv, 2400 København NV"]) { const v = await vask(tekst); console.log("datavask:", v.kategori, v.kandidater, v.sikker ? v.tekst : "lad brugeren vælge"); } console.log("reverse:", (await reverse(12.5683, 55.6761)).betegnelse); try { await adresse("00000000-0000-0000-0000-000000000000"); } catch (e) { if (!(e instanceof DanadresseError)) throw e; console.log("fejl:", e.status, e.code); } }
Output:
forslag: Rådhuspladsen 1, 4200 Slagelse forslag: Rådhuspladsen 1, st. tv, 4200 Slagelse forslag: Rådhuspladsen 1, st. th, 4200 Slagelse opslag: Rådhuspladsen 1, st. th, 4200 Slagelse 11.35623868 55.40464424 datavask: A 1 Strandvejen 100, 2900 Hellerup datavask: C 7 lad brugeren vælge reverse: Rådhuspladsen 3, 1550 København V fejl: 404 not_found
Hvad modulet gør
Timeout. AbortSignal.timeout(10_000) afbryder kaldet efter 10 sekunder. Uden den venter fetch i princippet uendeligt på en hængende forbindelse.
To fejlformater, én code. Fejl på DAWA-stierne har formen {type, title, details, code}. Fejl på /api/v1/* har {error: {code, message, details}}. Det gælder alle fejl, også 500. toError() læser begge og giver dig err.code, som er stabil, og err.details. Forgren på den, aldrig på teksten. Alle koder står i Fejlformatet.
429 og Retry-After. Minutgrænsen (rate_limit_per_minute) og grænsen pr. besøgende på browsernøgler (rate_limit_per_visitor) svarer med Retry-After, og modulet venter så længe og prøver igen. Retry-After er 1 til 60 sekunder, tiden til minutvinduet nulstilles. quota_exceeded betyder, at en gratiskonto har brugt sin månedskvote og bufferen, eller at en betalt konto har nået det budgetloft, den har sat. Det svar har ingen Retry-After, og modulet kaster fejlen med det samme: et nyt forsøg hjælper ikke før err.details.resets_at. Vis err.details.upgrade_url, eller alarmer dig selv. 503 upstream_unavailable kommer, når et offentligt register er midlertidigt nede, og har også Retry-After. Afviste kald tælles og faktureres ikke. Inden for bufferen (op til 2 400 kald) har svarene X-Quota-Grace: true.
Datavask. vask() returnerer kategorien fra topniveau (A, B eller C), antallet af kandidater, afstand og den bedste adresse. Kategorien findes kun på topniveau, og der er intet score-felt. A er en præcis match. B er præcis én sikker match med små stavefejl: gem den, med den officielle tekst. C er usikker: vis kandidaterne, og lad brugeren vælge. I demoen giver "Strandvejen 100, 2900 Hellerup" A med én kandidat, og "Rentemestervej 8, 2. tv, 2400 København NV" giver C med syv, fordi det nummer ikke har en 2. tv. betegnelse læser selv etage og dør.
Brug modulet i Express
Lad browseren kalde din egen server, så nøglen bliver på serveren. Ruten oversætter DanadresseError til et svar, din frontend kan bruge:
// server.mjs import express from "express"; import { autocomplete, adresse, DanadresseError } from "./danadresse.mjs"; const app = express(); app.get("/api/adresse/forslag", async (req, res, next) => { const q = String(req.query.q ?? "").trim(); if (q.length < 2) return res.json([]); try { const forslag = await autocomplete(q, 8); res.json(forslag.map((f) => ({ tekst: f.forslagstekst, id: f.data.id ?? null, type: f.type }))); } catch (e) { next(e); } }); app.get("/api/adresse/:id", async (req, res, next) => { try { res.json(await adresse(req.params.id)); } catch (e) { next(e); } }); app.use((err, req, res, next) => { if (err instanceof DanadresseError) { // 404 videre som 404. Alt andet er vores problem, ikke brugerens. const status = err.status === 404 ? 404 : 502; return res.status(status).json({ error: err.code ?? "upstream_error" }); } next(err); }); app.listen(3000);
For forslag af typen vejnavn er data.id tom, fordi brugeren ikke har valgt et husnummer endnu. Se Adresse eller adgangsadresse for forskellen på typerne.
npm-pakken @danadresse/client
Pakken er på npm (npm install @danadresse/client) og pakker de mest brugte endpoints ind:
| Metode | Kald |
|---|---|
autocomplete(q, { type, fuzzy, perSide }) | GET /autocomplete |
adresse(id) | GET /adresser/{id} |
adresser(params) | GET /adresser |
adgangsadresser(params) | GET /adgangsadresser |
reverse(x, y, srid) | GET /adgangsadresser/reverse |
datavask(betegnelse) | GET /datavask/adresser?betegnelse=… |
postnumre(params) | GET /postnumre |
enrichAll(id), enrichBbr(id), enrichEjendom(id) | GET /api/v1/enrich/adresser/{id}/… |
Konstruktøren tager apiKey, baseUrl og timeoutMs (standard 10 sekunder). Fejl kastes som DanadresseError med status og body, hvor body er svarteksten som streng.
Pakken prøver ikke igen af sig selv, og den giver dig ikke adgang til svar-headers, så du kan ikke læse Retry-After eller X-RateLimit-*. Fejlkroppen fra minutgrænsen har den samme ventetid i details.retry_after, så et lille retry-lag ovenpå kan bruge den:
// adresser.mjs import { DanadresseClient, DanadresseError } from "@danadresse/client"; const api = new DanadresseClient({ apiKey: process.env.DANADRESSE_API_KEY, timeoutMs: 8000, }); // Pakken giver ikke headers, men minutgrænsens fejlkrop har details.retry_after (sekunder) function retryAfterOf(err) { try { const body = JSON.parse(err.body); return (body.details ?? body.error?.details)?.retry_after ?? null; } catch { return null; } } export async function withRetry(fn, retries = 2) { for (let attempt = 0; ; attempt++) { try { return await fn(); } catch (err) { if (!(err instanceof DanadresseError) || attempt >= retries) throw err; // 429 uden retry_after er quota_exceeded: prøv aldrig igen const wait = err.status === 503 ? 10 : err.status === 429 ? retryAfterOf(err) : null; if (wait === null) throw err; await new Promise((r) => setTimeout(r, Math.min(wait, 30) * 1000)); } } } const hits = await withRetry(() => api.autocomplete("Rådhuspladsen 1, 4200", { type: "adresse", perSide: 5 })); const adr = await withRetry(() => api.adresse(hits[0].data.id)); const vask = await withRetry(() => api.datavask("Strandvejen 100, 2900 Hellerup")); const naer = await withRetry(() => api.reverse(12.5683, 55.6761)); console.log(adr.adressebetegnelse, vask.kategori, naer.adressebetegnelse);
Et par ting at vide om pakken:
- En timeout giver en
AbortErrorfrafetch, ikke enDanadresseError.withRetryovenfor prøver den ikke igen. Tilføj det, hvis du har brug for det. enrich*-metoderne virker ikke med testnøgler (403sandbox_scope) og ikke med browsernøgler (403browser_key_scope).- Har du brug for rate-limit-headers, fx til at drosle et batchjob ned, så brug fetch-modulet ovenfor.
Næste skridt
- Grænser, kvoter og 429 forklarer headers og grænser.
- Next.js route handler som proxy viser samme mønster i Next.js.
- Samme klient i Python: Python-klient med requests og httpx.
Relaterede guides
Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)