Skip to main content
All articles

/5 min read

Using the Fetch API in JavaScript

Beyond GET: JSON bodies, headers, credentials, CORS, cancellation with AbortController, streaming responses, and how Next.js extends fetch with caching controls.

By Juliano Alves

The Fetch API replaced XMLHttpRequest as the standard for HTTP from browser JavaScript and is now ubiquitous in Node 18+ and edge runtimes as well. It is promise-based, composes well with async/await, and pairs cleanly with modern cancellation and streaming APIs.

This post builds from a basic GET to production-minded patterns.

Basic GET with error handling#

fetch resolves on HTTP errors (404, 500)—you must check response.ok or response.status.

async function loadJson(url) {
  const response = await fetch(url, {
    method: 'GET',
    headers: { Accept: 'application/json' },
  });

  if (!response.ok) {
    const text = await response.text().catch(() => '');
    throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
  }

  return response.json();
}

Wrapping this once in your app avoids repeating boilerplate.

POST with JSON body#

const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Ada', role: 'admin' }),
});

For FormData, omit Content-Type—the browser sets the multipart boundary automatically.

Credentials and CORS#

  • credentials: 'include' sends cookies on cross-origin requests only if the server responds with proper Access-Control-Allow-Credentials and a non- * origin.
  • Same-site APIs often use same-origin (default for relative URLs in browsers).

Misconfigured CORS is a frequent source of “it works in Postman” bugs—always test from the browser origin.

Cancellation with AbortController#

Every in-flight request should be abortable when the user navigates away or retypes a search:

const controller = new AbortController();

fetch('/api/search?q=node', { signal: controller.signal })
  .then((r) => r.json())
  .then(console.log)
  .catch((err) => {
    if (err.name === 'AbortError') return; // expected
    throw err;
  });

// later: controller.abort();

In React, create the controller in useEffect and abort in the cleanup function.

Streaming responses#

For large downloads or NDJSON APIs, avoid await response.json()—use response.body as a ReadableStream:

const response = await fetch('/api/logs/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(decoder.decode(value, { stream: true }));
}

This keeps memory flat compared to buffering the entire payload.

Next.js and extended fetch#

In Server Components, Next extends fetch with next: { revalidate, tags } for caching. That is not standard browser fetch—keep mental separation between edge/server usage and client bundles.

Retries and timeouts#

Fetch has no built-in timeout. Combine AbortController with setTimeout or use a thin wrapper / library for retry policies (429 backoff, network errors).

Summary#

Master fetch by layering: status checks, JSON vs streams, AbortController, and credentials rules. In frameworks like Next, read the platform docs for caching extensions—those knobs change semantics beyond the web standard.

Have a project in mind?

contato@julianoalves.me

Rio de Janeiro · UTC−3