JavaScript Beginner

Fetch API: Making HTTP Requests in JavaScript

Learn how to use the Fetch API to make GET, POST, PUT, and DELETE requests. Includes CSRF handling for Django backends.

DjangoZen Team Mar 29, 2026 15 min read 167 views

Modern web applications constantly talk to servers — loading data, submitting forms, updating content without a page reload — and in the browser, the Fetch API is the standard tool for making those HTTP requests from JavaScript. It replaced the awkward older approaches with a clean, promise-based interface. Understanding fetch, promises, and async handling is essential for any interactive web work, and this tutorial covers it from the basics through the patterns you will use every day.

Why client-side requests matter

The interactivity users expect from modern websites — content that updates without reloading, forms that submit smoothly, data that loads on demand — all depends on JavaScript making HTTP requests to a server in the background and updating the page with the response. Without this, every interaction would require a full page reload. The Fetch API is how the browser makes these requests, so understanding it is fundamental to building dynamic interfaces. Whether you are loading a list of items, sending a form, or talking to an API, fetch is the mechanism, which is why it is one of the core skills for front-end and full-stack development alike.

A basic fetch request

The simplest fetch retrieves data from a URL. You call fetch with the URL, and it returns a promise that resolves to a response object representing the server's reply:

fetch("/api/items/")
    .then(response => response.json())
    .then(data => console.log(data));

This requests the URL, parses the JSON body of the response, and then works with the resulting data. The two-step parsing — first getting the response, then extracting its body — is characteristic of fetch. This basic pattern of fetching a URL and handling the returned data is the foundation that everything else builds on.

Promises and asynchronous code

Fetch is built on promises, which represent a value that will be available in the future — the eventual result of an operation that takes time, like a network request. A promise lets you attach handlers that run when the operation completes successfully or fails, rather than blocking while you wait. This is essential because network requests take time, and you do not want to freeze the page waiting for one. Understanding promises is necessary to understand fetch, because fetch returns a promise and you work with its result through promise handling. Promises are how JavaScript manages operations that complete later without halting everything else.

async and await

While you can handle promises with .then chains, modern JavaScript offers async and await, which let you write asynchronous code that reads like synchronous code. Inside an async function, await pauses until a promise resolves and gives you its value directly:

async function loadItems() {
    const response = await fetch("/api/items/");
    const data = await response.json();
    return data;
}

This is often clearer than chaining, because the steps read top to bottom. The async/await style is the preferred modern way to work with fetch, turning the flow of an asynchronous request into straightforward sequential-looking code.

Working with the response

The response object fetch gives you contains more than the data — it has the status code telling you whether the request succeeded, headers, and methods to extract the body in various formats. You parse the body according to its type, most commonly as JSON for API data, but also as text or other formats. Checking the status is important, because fetch does not treat an error status like a 404 or 500 as a failure on its own. Understanding that the response is a rich object you inspect and extract from — not just the data itself — is key to handling requests correctly, especially when you need to react to the status.

Handling errors properly

A common surprise with fetch is that it only rejects its promise on a network failure, not on an error HTTP status — a 404 or 500 response still resolves successfully as far as fetch is concerned. This means you must check the response's status yourself and handle error statuses explicitly, in addition to catching network errors. Proper error handling therefore involves both checking whether the response indicates success and catching genuine failures. Understanding this distinction prevents a frequent bug where code assumes a resolved fetch means success and then breaks on an error response, and it is essential for building requests that handle real-world failures gracefully.

Sending data with POST

Beyond retrieving data, you frequently need to send it — submitting a form, creating a record. For this you configure the fetch with a method like POST, a body containing the data (often as JSON), and appropriate headers indicating the content type:

await fetch("/api/items/", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({name: "New item"})
});

This sends data to the server to create or update something. Understanding how to configure the method, headers, and body is what lets you do the full range of operations — not just reading data but writing it — which covers the interactive form submissions and updates that dynamic applications depend on.

Headers and content types

Headers carry metadata about a request and response — the content type of the data, authentication tokens, and more — and setting them correctly matters. When sending JSON, you indicate that with a content-type header so the server knows how to interpret the body; when authenticating, you include the appropriate header carrying the token. The server uses these to process your request correctly. Understanding that headers communicate important context about your request, and setting the right ones for what you are doing, is necessary for requests to work as intended, particularly when interacting with APIs that expect specific headers for content type or authentication.

Working with Django and CSRF

When your JavaScript talks to a Django backend, there is an important detail: Django protects against cross-site request forgery, and requests that change data must include a CSRF token. For fetch requests that submit data to Django, you include this token in a header so the request passes the protection. This is a common stumbling point when connecting front-end JavaScript to a Django backend, where POST requests are rejected until the token is included. Knowing that Django's CSRF protection requires this token on state-changing requests, and how to include it in your fetch headers, is essential for the very common case of a JavaScript front-end communicating with a Django server.

Building a reusable request helper

As an application grows, repeating the same fetch configuration everywhere becomes tedious and error-prone, so it is common to wrap fetch in a small helper that centralizes the common setup — base URL, default headers, error handling, JSON parsing, and any authentication or CSRF tokens. Then each request is a concise call to the helper rather than a full fetch configuration. This keeps your request code DRY and consistent, and ensures error handling and required headers are applied everywhere. Building such a helper is a natural step once you understand fetch, turning repetitive request code into clean, reusable calls and centralizing the concerns that every request shares.

Loading and error states in the UI

Because requests take time and can fail, a good interface reflects this to the user. While a request is in flight, showing a loading indicator tells the user something is happening rather than leaving them staring at an unresponsive page; when a request fails, showing a clear error message lets them understand and retry. Managing these states — loading, success, error — around your fetch calls is part of building a polished experience. Understanding that a request is not instantaneous or guaranteed, and that reflecting its state in the interface matters, is what turns a technically working fetch into a feature that feels responsive and trustworthy to the person using it.

Updating the page with results

The point of fetching data is usually to display it, so after a request returns you update the page with the result — inserting the loaded items into a list, replacing content, or reflecting a change. This connects the network request to what the user actually sees, taking the data you received and rendering it into the interface. The combination of fetching data and updating the page in response is the essence of dynamic, no-reload interactivity. Understanding that fetch is one half of the pattern — getting the data — and updating the DOM is the other half — showing it — completes the picture of how modern interactive features work in the browser.

Cancelling requests

Sometimes a request becomes unnecessary before it completes — the user navigates away, or types a new search before the previous one returns — and continuing it wastes resources or causes confusing results. The browser provides a way to abort a fetch request, which is useful for cancelling outdated requests, such as superseded searches in a live-search feature. Knowing that requests can be cancelled, and that doing so prevents stale responses from arriving and updating the page incorrectly, is a refinement that matters for interfaces with rapid interactions. It is part of handling the asynchronous nature of requests cleanly, ensuring only relevant responses affect what the user sees.

Security considerations

Making requests from the browser involves security considerations worth understanding. The browser enforces rules about which origins a page can make requests to, and servers control this through their configuration, which is why a request to a different domain may be blocked unless that server permits it. When sending data, including authentication securely and the CSRF token for protected backends matters. Being aware of these browser security mechanisms — cross-origin rules and the need to handle authentication and CSRF correctly — helps you understand why some requests behave as they do and how to make requests that work within the browser's security model rather than being mysteriously blocked.

Summary

The Fetch API is the standard way JavaScript makes HTTP requests in the browser, powering the dynamic, no-reload interactivity modern web applications depend on. It is promise-based: a fetch returns a promise resolving to a response object, and modern code handles this cleanly with async and await, which make asynchronous requests read like sequential steps. You extract the body from the response in the format you need, usually JSON, and crucially you must check the status yourself because fetch does not treat error statuses as failures. Sending data uses a configured request with a method, headers, and body, where setting the right content-type and authentication headers matters — and when talking to Django, including the CSRF token on state-changing requests is essential. As you grow, wrapping fetch in a reusable helper centralizes this setup. Master fetch, promises, async handling, and proper error checking, and you have the core skill for building interactive web applications that communicate with servers reliably.