6 grunde til at dit næste API-kald skal vælge mellem fetch og Axios

6 grunde til at dit næste API-kald skal vælge mellem fetch og Axios

“Jeg installerer bare Axios, det gør alle andre”

Det var nogenlunde sådan en studerende sagde til mig på et frontend-hold, da vi skulle lave de første API-kald til en lille Node backend. Jeg spurgte hvorfor. Han trak på skuldrene og sagde: “Det er jo sådan man gør”.

Det er lidt som at købe en kæmpe espressomaskine, før du har prøvet at hælde kogende vand gennem en tragt. Du kan godt, men du ved ikke helt, hvad du betaler for.

I JavaScript-verdenen er diskussionen stort set altid: fetch vs Axios. Men spørgsmålet burde være: hvad har du faktisk brug for?

1: Hvad du reelt har brug for fra dit HTTP-værktøj

Hvis vi skræller alt støjen væk, handler valget om 6 ting:

  • Hvordan du laver simple GET/POST requests
  • Hvordan du håndterer fejl (statuskoder, netværk, JSON)
  • Om du har brug for timeouts
  • Om du vil have interceptors eller en slags middleware
  • Om du skal lave retries og håndtere rate limits pænt
  • Hvordan koden ser ud, når du har 50+ kald og ikke bare 2

fetch er indbygget i browseren. Intet ekstra dependency, ingen bundle-størrelse, ingen magi.

Axios er et bibliotek, der pakker de samme ting ind, og tilføjer komfort-funktioner som timeouts, interceptors og automatisk JSON-parsing.

Min erfaring: hvis du ikke kender fetch godt, ender du med at bruge Axios til at løse problemer, du egentlig kunne have løst med 10 linjer kode selv.

2: Side om side – GET og POST med fetch vs Axios

GET request med fetch

// Simpel GET med fetch
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);

  if (!res.ok) {
    // res.ok er false hvis status er 4xx eller 5xx
    throw new Error(`HTTP fejl: ${res.status}`);
  }

  const data = await res.json();
  return data;
}

GET request med Axios

import axios from 'axios';

async function getUser(id) {
  const res = await axios.get(`/api/users/${id}`);
  // Axios parser JSON automatisk og lægger det på res.data
  return res.data;
}

Axios-versionen er lidt kortere, fordi den:

  • smider en fejl automatisk ved 4xx/5xx
  • parser JSON for dig

Men det er ikke raketvidenskab at gøre det samme med fetch. Vi vender tilbage til det i wrapper-sektionen.

POST request med fetch

async function createUser(user) {
  const res = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  });

  if (!res.ok) {
    throw new Error(`HTTP fejl: ${res.status}`);
  }

  return res.json();
}

POST request med Axios

async function createUser(user) {
  const res = await axios.post('/api/users', user);
  return res.data;
}

Axios gætter automatisk Content-Type og serialiserer til JSON, når du giver den et objekt. fetch kræver at du er eksplicit.

Personligt kan jeg godt lide at være eksplicit, når jeg arbejder med HTTP. Men når du har 40 endpoints, begynder det at støje, hvis du ikke abstraherer det væk.

3: Fejlhåndtering i fetch vs Axios

Det mærkelige ved fetch

fetch opfører sig lidt kontraintuitivt første gang:

  • fetch smider kun en fejl ved netværksfejl (ingen forbindelse, CORS block osv.)
  • fetch synes at 404, 500 osv. er helt fint – det er bare et svar

Det betyder, at du altid selv skal tjekke res.ok eller res.status.

Typisk fetch-fejl

// Typisk fejl: glemmer at tjekke res.ok
const res = await fetch('/api/users/123');
const data = await res.json();
// Hvis serveren svarede 500, ender du måske med kryptiske fejl senere

Axios og fejl

Axios har en anden model:

  • Axios smider en fejl, hvis status er uden for 2xx
  • Fejlen har et error.response-objekt med status, data osv.
try {
  const res = await axios.get('/api/users/999');
  return res.data;
} catch (err) {
  if (err.response) {
    console.log('Status:', err.response.status);
    console.log('Body:', err.response.data);
  } else {
    console.log('Netværksfejl', err.message);
  }
}

JSON parsing og fejl

Både fetch og Axios kan ramme dette klassiske problem:

  • Serveren returnerer ikke valid JSON
  • Du kalder res.json() (fetch) eller forventer JSON (Axios)
  • Du får en parsing-fejl

fetch:

const res = await fetch('/api/weird');

try {
  const data = await res.json();
} catch (err) {
  console.error('Kunne ikke parse JSON', err);
}

Axios:

try {
  const res = await axios.get('/api/weird');
  console.log(res.data);
} catch (err) {
  if (err.response) {
    // Her kan err.response.data nogen gange være en string
    console.log('Rå body:', err.response.data);
  }
}

Min anbefaling: lav altid et lille lag ovenpå både fetch og Axios, hvor du håndterer:

  • statuskoder
  • JSON parsing
  • fejlformat (så resten af din app får den samme fejlstruktur)

Det er den wrapper vi bygger senere.

4: Timeouts og abort – fetch AbortController vs Axios timeout

Problem: hængende requests

Hvis du laver API-kald til noget, du ikke helt stoler på (eksterne services, dårlige wifi-forbindelser, langsomme testservere), får du før eller siden hængende requests.

Du har to ønsker:

  • Stop requestet efter X sekunder
  • Kun opdatér UI’et, hvis det er det seneste request

fetch og AbortController

fetch understøtter abort via AbortController. Det lyder dramatisk, men det er ret lige til:

async function getWithTimeout(url, { timeoutMs = 5000 } = {}) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(id);

    if (!res.ok) {
      throw new Error(`HTTP fejl: ${res.status}`);
    }

    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw err;
  }
}

Nu har du faktisk en timeout på fetch, selv om den ikke er “indbygget” som option.

Axios timeout

Axios har timeout indbygget som konfiguration:

const api = axios.create({
  baseURL: '/api',
  timeout: 5000
});

async function getUser(id) {
  const res = await api.get(`/users/${id}`);
  return res.data;
}

Hvis timeout rammer, får du en fejl hvor err.code === 'ECONNABORTED'. Du kan fange og håndtere det i en interceptor eller lokalt.

Hvilken model føles bedst?

fetch kræver mere manuel opsætning, men giver dig ret meget kontrol. Axios gør det nemt at sætte en global timeout, især i større apps.

Hvis du arbejder i ren browser-frontend uden kæmpe arkitektur, synes jeg ofte AbortController + en lille helper-funktion er fint.

5: Interceptors og middleware – hvor Axios trækker fra

Hvad er interceptors?

Interceptors er små stykker kode, der kører før en request sendes, eller før et svar gives videre til din app.

Typiske brugsscenarier:

  • Automatisk sætte Authorization-header
  • Logge alle fejl ét sted
  • Automatisk refresh af tokens ved 401

Axios interceptors eksempel

const api = axios.create({ baseURL: '/api' });

// Request interceptor - tilføj token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - håndter 401 ét sted
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status === 401) {
      // fx redirect til login eller prøve at refreshe token
      console.warn('Unauthorized, logger bruger ud');
    }
    return Promise.reject(error);
  }
);

Nu kan resten af din kode bare skrive api.get('/users') uden at tænke på tokens og 401 hver gang.

Kan man gøre noget lignende med fetch?

Ja, men du skal bygge det selv. Typisk med en lille request()-funktion, som alle fetch-kald går igennem.

async function request(path, options = {}) {
  const token = localStorage.getItem('token');

  const headers = {
    'Content-Type': 'application/json',
    ...(options.headers || {}),
  };

  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }

  const res = await fetch(`/api${path}`, {
    ...options,
    headers,
  });

  if (!res.ok) {
    if (res.status === 401) {
      console.warn('Unauthorized, logger bruger ud');
    }
    const text = await res.text();
    throw new Error(`HTTP ${res.status}: ${text}`);
  }

  const text = await res.text();
  return text ? JSON.parse(text) : null;
}

Her har du bygget din egen “interceptor light”.

Hvis du kan leve med at bygge sådan et lag én gang, er fetch stadig helt fint.

6: Retries og rate limits

Når du rammer eksterne API’er, vil du før eller siden:

  • ramme 429 (for mange requests)
  • opleve midlertidige 5xx-fejl

Her er en lille retry-strategi med fetch:

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url, options);

      if (res.status === 429 && attempt < maxRetries) {
        const retryAfter = res.headers.get('Retry-After');
        const delayMs = retryAfter 
          ? Number(retryAfter) * 1000 
          : (attempt + 1) * 1000;
        await new Promise((r) => setTimeout(r, delayMs));
        continue;
      }

      if (!res.ok) {
        throw new Error(`HTTP ${res.status}`);
      }

      return res.json();
    } catch (err) {
      if (attempt === maxRetries) throw err;
      await new Promise((r) => setTimeout(r, (attempt + 1) * 1000));
    }
  }
}

Axios har ikke indbygget retries, men der findes plugins som axios-retry. Det er så også endnu et dependency.

Spørgsmålet er igen: vil du have alt færdigpakket, eller vil du selv have kontrol over strategien?

7: Beslutningsregel – 3 gange fetch er nok, 3 gange Axios giver mening

Vælg fetch hvis:

  • 1. Du bygger en lille eller mellemstor frontend
    Et par håndfulde endpoints, ingen vanvittig auth-flow, ingen ekstreme krav til retries.
  • 2. Du vil minimere dependencies
    Mindre bundle, færre opdateringer at holde styr på, og du vil hellere lære browserens API’er ordentligt. God idé hvis du lige er i gang med at lære JavaScript og gerne vil forstå grundstenene.
  • 3. Du er okay med at skrive en lille wrapper selv
    Som den vi om lidt bygger, så din kode ikke sejler ud i fetch(...) over det hele.

Vælg Axios hvis:

  • 1. Du bygger en større SPA eller frontend med meget API-trafik
    Især hvis du har komplekse auth-flows, hvor interceptors sparer dig for meget gentagelse.
  • 2. Du har mange teams der deler kode
    Axios kan være rar, når du vil have en fælles klient med ens fejlformat, timeouts og headers, uden at alle skal forstå alle fetch-detaljer.
  • 3. Du kommer fra Node/backendlageret
    Eller du arbejder både i browser og Node. Axios føles ofte mere “hjemligt” og fungerer på tværs uden for meget bøvl.

Hvis du er i tvivl, starter jeg personligt med fetch og en wrapper. Hvis det begynder at gøre ondt, kan du altid skifte til Axios senere og beholde dit API-lag nogenlunde intakt.

8: En lille standard-wrapper til fetch og til Axios

Nu vi har snakket så meget om wrappers, er det på tide at bygge to små stykker kode, du faktisk kan bruge.

Idéen er den samme i begge tilfælde:

  • ens entrypoint til API-kald
  • central fejlhåndtering
  • mulighed for at sætte auth, baseURL osv. ét sted

Wrapper til fetch

// apiClient.js

const BASE_URL = '/api';

async function apiRequest(path, { method = 'GET', body, headers = {}, timeoutMs = 8000 } = {}) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeoutMs);

  const token = localStorage.getItem('token');

  const finalHeaders = {
    'Content-Type': 'application/json',
    ...headers,
  };

  if (token) {
    finalHeaders.Authorization = `Bearer ${token}`;
  }

  const options = {
    method,
    headers: finalHeaders,
    signal: controller.signal,
  };

  if (body !== undefined) {
    options.body = JSON.stringify(body);
  }

  try {
    const res = await fetch(BASE_URL + path, options);
    clearTimeout(id);

    const text = await res.text();
    const data = text ? JSON.parse(text) : null;

    if (!res.ok) {
      const error = new Error('API error');
      error.status = res.status;
      error.data = data;
      throw error;
    }

    return data;
  } catch (err) {
    if (err.name === 'AbortError') {
      const timeoutError = new Error('API timeout');
      timeoutError.code = 'TIMEOUT';
      throw timeoutError;
    }
    throw err;
  }
}

// Små helpers
export function get(path, options) {
  return apiRequest(path, { ...options, method: 'GET' });
}

export function post(path, body, options) {
  return apiRequest(path, { ...options, method: 'POST', body });
}

export function put(path, body, options) {
  return apiRequest(path, { ...options, method: 'PUT', body });
}

export function del(path, options) {
  return apiRequest(path, { ...options, method: 'DELETE' });
}

Brug i din app:

import { get, post } from './apiClient';

async function loadUser(id) {
  try {
    const user = await get(`/users/${id}`);
    console.log(user);
  } catch (err) {
    if (err.code === 'TIMEOUT') {
      // Vis "prøv igen" besked
    }
    console.error(err);
  }
}

async function saveUser(user) {
  return post('/users', user);
}

Wrapper til Axios

// apiClientAxios.js
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  timeout: 8000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor - token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - normaliser fejlformat
api.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.code === 'ECONNABORTED') {
      const timeoutError = new Error('API timeout');
      timeoutError.code = 'TIMEOUT';
      throw timeoutError;
    }

    if (error.response) {
      const err = new Error('API error');
      err.status = error.response.status;
      err.data = error.response.data;
      throw err;
    }

    throw error;
  }
);

export function get(path, config) {
  return api.get(path, config);
}

export function post(path, body, config) {
  return api.post(path, body, config);
}

export function put(path, body, config) {
  return api.put(path, body, config);
}

export function del(path, config) {
  return api.delete(path, config);
}

Brug i din app er nu næsten identisk med fetch-wrapperen. Det er hele pointen. Hvis du senere vil skifte fra fetch til Axios eller omvendt, rører du mest din egen apiClient-fil.

Det samme princip gælder også, hvis du en dag vil prøve et af de nyere fetch-agtige biblioteker eller splitte din kode op i moduler og dele API-klienten mellem projekter.

Hvor står vi så i 2026?

fetch er blevet markant bedre støttet end for få år siden. AbortController findes overalt, og der er færre grunde til at tage Axios ind for at få et “moderne” API.

Axios er stadig stærk, når du bygger større ting med mange teams, tunge auth-flows og komplekse behov for interceptors og fælles klientlogik.

Spørgsmålet er måske ikke så meget: “fetch vs Axios – hvad er bedst?” men mere: hvor længe kan du holde din egen API-klient så enkel, at du rent faktisk forstår den?

Om et par år tror jeg, at flere vil starte med fetch, bygge en lille wrapper, og først bagefter vælge om de vil have et bibliotek ovenpå. Hvilken vej vil du gå i dit næste projekt?

Med fetch bruger du AbortController: opret en controller, giv dens signal til fetch og kald controller.abort() efter en setTimeout for at indføre timeout. Axios har en timeout-option (ms) og understøtter også AbortController i nyere versioner; ældre Axios-brugere kender CancelToken. Husk at rense timers og håndtere den specifikke fejltype/meddelelse ved afbrud.
Brug en eksisterende retry-løsning i stedet for at rulle din egen: axios-retry eller retry-axios til Axios, og ky eller p-retry til fetch, som også kan håndtere eksponentiel backoff. Respekter Retry-After-headeren, undgå at automatisk genkøre ikke-idempotente requests (som POST) uden ekstra logik, og sæt et maksimalt antal forsøg.
Node har global fetch fra version 18; hvis du understøtter ældre Node-udgaver skal du bruge en polyfill som undici eller node-fetch. I browseren findes fetch i alle moderne browsere, men Internet Explorer kræver polyfill; Axios virker både i browser og Node uden ekstra polyfills.
Axios har gode generiske typer, så du kan skrive axios.get(...) og få typed res.data direkte. fetch returnerer et generisk Response-objekt, så du typisk parse'er og caster eller bygger små wrappers/validators (fx zod) for sikker typning af body-indhold.

Lasse Falkenberg er typen, der begyndte at rode med HTML og CSS for at lave en simpel bandside – og opdagede, at det var langt sjovere at få knapperne til at virke end at stå på scenen. Siden har han kastet sig over alt fra små JavaScript-snippets til Python-scripts, der kan spare ham for kedeligt, manuelt arbejde i hverdagen.

Han har lært det meste ved at bygge ting, der lige præcis løser hans egne problemer: en lille webapp til at holde styr på brætspilsaftener, et script til at rydde op i rodede mapper, eller en enkel side til at dele noter med venner. Undervejs har han kæmpet sig gennem alle de klassiske fejl – semikolon, forkerte indrykninger og variabler, der hedder noget helt andet end man tror – og det er præcis den rejse, han deler på Coding Class.

På Coding Class skriver Lasse praktiske, jordnære guides, der tager udgangspunkt i små, konkrete opgaver: noget du kan se, teste og bygge videre på med det samme. Han elsker at bryde en opgave ned i små bidder, vise den fulde kode og forklare linje for linje, hvad der sker – inklusive de typiske bugs, du med stor sandsynlighed også støder på.

For Lasse handler kodning ikke om flotte titler eller store ord, men om følelsen af at få noget til at virke – og om at du som læser kan gå derfra med noget, du selv har bygget. Hvis du kan kende glæden ved at få en fejl til endelig at forsvinde, er du lige på bølgelængde med hans måde at lære fra sig på.

Send kommentar

You May Have Missed