Fetch

The browser's fetch function sends HTTP requests from JavaScript and returns a promise of the response. Chain .then to read the JSON, check response.ok before trusting it, and pass an options object to POST, PUT or DELETE.

Apprentice · JavaScript · REST API · Updated

After this lesson you can

  • Send a GET request with fetch, and read the JSON from the response with then
  • Send POST, PUT and DELETE requests with fetch, with a JSON body and a Content-Type header, and check response.ok

Key takeaways

  • fetch(url) sends a GET and returns a promise; .then says what to do when it's kept, and response.json() reads the JSON body.
  • fetch keeps its promise even for a 404 or a 500, so check response.ok before trusting the response.
  • To send data, pass an options object with the method, a Content-Type: application/json header and body: JSON.stringify(item).
  • .catch runs when a then throws, and when there's no answer at all, such as when the server is down.

In Calling APIs from C#, HttpClient called your categories API from a C# program. A web page can call it too, from JavaScript, with a function built into every browser: fetch.

Putting the page beside the API

A page can call an API freely when both come from the same origin: the same address and port. So the simplest setup is to let the API serve the page. Add a wwwroot folder to the NorthwindApi project from HTTP and Web APIs, and two lines to its Program.cs, before app.UseHttpsRedirection();:

app.UseDefaultFiles();
app.UseStaticFiles();

UseStaticFiles serves the files in wwwroot, and UseDefaultFiles sends wwwroot/index.html when someone asks for /. Now http://localhost:5179/ is your page, and /api/categories is your API, on the same origin.

(A page from somewhere else, such as an Angular app on its own port, is blocked by the browser unless the API says that origin is allowed. That's called CORS. When you build an Angular app, you'll see a simpler way around it.)

Open the page, press F12, and you can run every example in this lesson in the Console.

A GET with fetch

fetch('/api/categories/8')
    .then(response => response.json())
    .then(category => console.log(`${category.categoryName}: ${category.description}`));
Seafood: Seaweed and fish

fetch('/api/categories/8') sends a GET to the page's own origin. But a request takes time, and JavaScript doesn't stop and wait. Instead, fetch returns a promise: an object that promises a result later, like a C# Task.

.then(callback) says what to do when the promise is kept. Here there are two steps:

  1. The first then gets the response: status, headers and a body still arriving. response.json() reads the body and parses the JSON, and it returns another promise.
  2. The second then gets the parsed object, category, with the same camelCase names as the JSON.

Each then hands its result to the next, so the steps line up in order, like a LINQ chain.

Checking the response

fetch keeps its promise whenever the server answers, even with a 404 or a 500. So check response.ok, which is true for 2xx codes, like IsSuccessStatusCode:

fetch('/api/categories/300')
    .then(response => {
        console.log(response.status, response.ok);
        if (!response.ok) {
            throw new Error(`The API answered ${response.status}`);
        }
        return response.json();
    })
    .then(category => console.log(category.categoryName))
    .catch(error => console.log(error.message));
404 false
The API answered 404
  • Throwing inside a then skips the rest of the chain and jumps to .catch(callback), like a catch block.
  • .catch also runs when there's no answer at all: the server is down, or the network failed. Then fetch itself breaks its promise, like HttpRequestException.

Sending data: POST, PUT and DELETE

To send anything but a GET, give fetch a second argument: an options object with the method, the headers and the body. The body must be text, so JSON.stringify turns your object into JSON:

const snacks = { categoryName: 'Snacks', description: 'Chips, nuts and pretzels' };

fetch('/api/categories', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(snacks)
})
    .then(response => {
        console.log(response.status);
        return response.json();
    })
    .then(added => console.log(`Added with id ${added.categoryId}`));
201
Added with id 9

Without the Content-Type header, the API wouldn't know the body is JSON, and would answer 415 Unsupported Media Type.

PUT works the same way, and answers with the category as it's now saved, so there's JSON to read, as after a POST. A DELETE sends nothing back: there's just the status to check.

const changed = { categoryId: 9, categoryName: 'Snacks', description: 'Chips, nuts, pretzels and popcorn' };

fetch('/api/categories/9', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(changed)
})
    .then(response => {
        console.log(`PUT: ${response.status}`);
        return response.json();
    })
    .then(saved => console.log(`Now: ${saved.description}`))
    .then(() => fetch('/api/categories/9', { method: 'DELETE' }))
    .then(response => console.log(`DELETE: ${response.status}`));
PUT: 200
Now: Chips, nuts, pretzels and popcorn
DELETE: 204

The first then returns response.json()'s promise, so the second gets the saved category. The third then returns a new fetch's promise, so the DELETE waits until the PUT is done, and the last then gets the DELETE's response.

To fetch options
Read none: fetch(url) is a GET
Create method: 'POST', a Content-Type header, body: JSON.stringify(item)
Replace method: 'PUT', a Content-Type header, body: JSON.stringify(item)
Delete method: 'DELETE'

Check yourself

2 questions to try on the lesson page.

So what

fetch is HttpClient for the browser: a URL, a method, maybe a JSON body, and a promise of the response. Chains of then read nicely for one or two steps; the next lesson shows a way to write longer ones that reads like ordinary code.

Key terms

Cheat sheets