In the modern Jurassic Park, in addition to local monitoring and control systems, we also need to communicate with external systems and APIs. Whether checking weather forecasts for the park, synchronizing data with global dinosaur databases, or integrating with visitor booking systems — all of this requires a solid method for making HTTP requests. In this exercise we will learn about the Fetch API, the modern interface for making network requests in JavaScript.
The Fetch API is a modern, built-in JavaScript interface for making HTTP requests. It replaces older approaches such as XMLHttpRequest, offering a more ergonomic and powerful Promise-based toolset.
Main advantages of the Fetch API:
The basic Fetch syntax is very simple:
1fetch(url)
2 .then(response => response.json())
3 .then(data => console.log(data))
4 .catch(error => console.error('Error:', error));Let's see how we could use Fetch in our Jurassic Park:
1// Fetching dinosaur data from the API
2function fetchDinosaurData(id) {
3 return fetch(`https://api.jurassic-park.com/dinosaurs/${id}`)
4 .then(response => {
5 // Check that the response is successful
6 if (!response.ok) {
7 throw new Error(`HTTP Error: ${response.status}`);
8 }
9 return response.json();
10 })
11 .then(data => {
12 console.log(`Fetched dinosaur data: ${data.name}`);
13 return data;
14 })
15 .catch(error => {
16 console.error(`Failed to fetch dinosaur #${id} data:`, error);
17 throw error;
18 });
19}
20
21// Using the function
22fetchDinosaurData(42)
23 .then(dino => {
24 console.log(`Dinosaur #42: ${dino.name}, Species: ${dino.species}`);
25 updateInfoPanel(dino);
26 })
27 .catch(error => {
28 displayErrorMessage(`Failed to fetch dinosaur info: ${error.message}`);
29 });Fetch performs a GET request by default, but we can easily specify a different request type and send data:
1// Updating dinosaur data with a PUT request
2async function updateDinosaurData(id, newData) {
3 try {
4 const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
5 method: 'PUT',
6 headers: {
7 'Content-Type': 'application/json',
8 'Authorization': `Bearer ${apiToken}`
9 },
10 body: JSON.stringify(newData)
11 });
12
13 if (!response.ok) {
14 throw new Error(`Update error: ${response.status}`);
15 }
16
17 const updatedDinosaur = await response.json();
18 console.log(`Updated dinosaur #${id} data`);
19 return updatedDinosaur;
20 } catch (error) {
21 console.error(`Error updating dinosaur #${id}:`, error);
22 throw error;
23 }
24}
25
26// Creating a new dinosaur record with a POST request
27async function addNewDinosaur(data) {
28 try {
29 const response = await fetch('https://api.jurassic-park.com/dinosaurs', {
30 method: 'POST',
31 headers: {
32 'Content-Type': 'application/json',
33 'Authorization': `Bearer ${apiToken}`
34 },
35 body: JSON.stringify(data)
36 });
37
38 if (!response.ok) {
39 throw new Error(`Error adding dinosaur: ${response.status}`);
40 }
41
42 const newDinosaur = await response.json();
43 console.log(`Added new dinosaur #${newDinosaur.id}`);
44 return newDinosaur;
45 } catch (error) {
46 console.error('Error adding dinosaur:', error);
47 throw error;
48 }
49}
50
51// Deleting a dinosaur with a DELETE request
52async function deleteDinosaur(id) {
53 try {
54 const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
55 method: 'DELETE',
56 headers: {
57 'Authorization': `Bearer ${apiToken}`
58 }
59 });
60
61 if (!response.ok) {
62 throw new Error(`Error deleting dinosaur: ${response.status}`);
63 }
64
65 // For DELETE the server may not return any data
66 if (response.status !== 204) { // No Content
67 const data = await response.json();
68 console.log(`Deleted dinosaur #${id}:`, data);
69 return data;
70 }
71
72 console.log(`Deleted dinosaur #${id}`);
73 return { success: true, id };
74 } catch (error) {
75 console.error(`Error deleting dinosaur #${id}:`, error);
76 throw error;
77 }
78}When we call
fetch(), it returns a Promise that resolves to a Response object. This object contains all information about the HTTP response:1fetch('https://api.jurassic-park.com/status')
2 .then(response => {
3 console.log('Status:', response.status); // e.g. 200
4 console.log('Status OK:', response.ok); // true for status 200-299
5 console.log('Content type:', response.headers.get('content-type'));
6 console.log('All headers:', [...response.headers.entries()]);
7
8 return response.json(); // Parse response as JSON
9 })
10 .then(data => {
11 console.log('Data:', data);
12 });The
Response object offers various methods for processing response data:| Method | Description | |--------|-------------| | response.json() | Parses content as JSON and returns a Promise with the result | | response.text() | Returns a Promise with content as text | | response.blob() | Returns a Promise with content as a Blob object (binary data) | | response.formData() | Parses content as FormData | | response.arrayBuffer() | Returns a Promise with content as ArrayBuffer |
It is important to remember that you can only call one of these methods on a
Response object. The response body can only be processed once — trying to call response.json() multiple times will throw an error.An important characteristic of the Fetch API is that the Promise returned by
fetch() will not be rejected for HTTP errors such as 404 or 500. It will only be rejected for network problems such as no connection.To properly handle HTTP errors, you need to check the
response.ok property or the response.status code:1fetch('https://api.jurassic-park.com/dinosaurs/999')
2 .then(response => {
3 if (!response.ok) {
4 throw new Error(`HTTP Error: ${response.status}`);
5 }
6 return response.json();
7 })
8 .then(data => {
9 console.log('Data:', data);
10 })
11 .catch(error => {
12 console.error('Error:', error.message);
13 // Here we can handle both HTTP errors and network errors
14 });You can expand this logic to better handle different status codes:
1fetch('https://api.jurassic-park.com/dinosaurs/999')
2 .then(response => {
3 if (response.status === 404) {
4 throw new Error('Dinosaur not found');
5 } else if (response.status === 401) {
6 throw new Error('Unauthorized — please log in again');
7 } else if (response.status === 403) {
8 throw new Error('No permission for this operation');
9 } else if (!response.ok) {
10 throw new Error(`HTTP Error: ${response.status}`);
11 }
12 return response.json();
13 })
14 .then(/* ... */)
15 .catch(/* ... */);Fetch accepts an optional second argument that allows customizing the request:
1fetch(url, {
2 method: 'POST', // HTTP method (GET, POST, PUT, DELETE, etc.)
3 headers: { // HTTP headers
4 'Content-Type': 'application/json',
5 'Authorization': 'Bearer token123'
6 },
7 body: JSON.stringify(data), // Request body (for POST, PUT, etc.)
8 mode: 'cors', // CORS mode
9 credentials: 'include', // Whether to include cookies
10 cache: 'no-cache', // Cache handling strategy
11 redirect: 'follow', // How to handle redirects
12 referrer: 'no-referrer', // Referrer header
13 signal: abortController.signal // For cancelling the request
14})1async function makeComplexRequest() {
2 const abortController = new AbortController();
3 const signal = abortController.signal;
4
5 // Set a timeout for the request
6 const timeout = setTimeout(() => {
7 abortController.abort();
8 }, 5000); // 5 seconds
9
10 try {
11 const response = await fetch('https://api.jurassic-park.com/data', {
12 method: 'POST',
13 headers: {
14 'Content-Type': 'application/json',
15 'Authorization': `Bearer ${apiToken}`,
16 'X-Jurassic-Version': '1.2'
17 },
18 body: JSON.stringify({
19 query: 'velociraptor',
20 limit: 10,
21 withDetails: true
22 }),
23 mode: 'cors',
24 credentials: 'include', // Include cookies
25 cache: 'no-cache', // Do not use cache
26 redirect: 'follow', // Automatically follow redirects
27 referrer: 'no-referrer',
28 signal // For cancelling the request
29 });
30
31 clearTimeout(timeout);
32
33 if (!response.ok) {
34 throw new Error(`HTTP Error: ${response.status}`);
35 }
36
37 return await response.json();
38 } catch (error) {
39 clearTimeout(timeout);
40
41 if (error.name === 'AbortError') {
42 console.error('Request aborted after timeout');
43 throw new Error('Request timed out');
44 }
45
46 console.error('Request error:', error);
47 throw error;
48 }
49}AbortController is an interface that allows cancelling Fetch requests. It is especially useful for implementing timeouts or when the user wants to cancel a long-running request:
1function fetchLargeDinosaurData(query) {
2 // Create a cancellation controller
3 const controller = new AbortController();
4 const { signal } = controller;
5
6 // Set timeout
7 const timeoutId = setTimeout(() => {
8 controller.abort();
9 }, 10000); // 10 seconds
10
11 // Cancel button for the user
12 const cancelButton = document.getElementById('cancel-button');
13 cancelButton.addEventListener('click', () => {
14 controller.abort();
15 clearTimeout(timeoutId);
16 cancelButton.disabled = true;
17 cancelButton.textContent = 'Cancelled';
18 });
19
20 // Make the request with the abort signal
21 return fetch(`https://api.jurassic-park.com/search?q=${query}`, { signal })
22 .then(response => {
23 clearTimeout(timeoutId);
24 if (!response.ok) {
25 throw new Error(`HTTP Error: ${response.status}`);
26 }
27 return response.json();
28 })
29 .then(data => {
30 cancelButton.disabled = true;
31 cancelButton.textContent = 'Done';
32 return data;
33 })
34 .catch(error => {
35 clearTimeout(timeoutId);
36
37 if (error.name === 'AbortError') {
38 console.log('Request was cancelled');
39 return { cancelled: true };
40 }
41
42 throw error;
43 });
44}Sometimes we need to make multiple requests at the same time:
1async function fetchPaddockData(paddockId) {
2 try {
3 const dinosaursPromise = fetch(`https://api.jurassic-park.com/paddocks/${paddockId}/dinosaurs`).then(r => r.json());
4 const statusPromise = fetch(`https://api.jurassic-park.com/paddocks/${paddockId}/status`).then(r => r.json());
5 const historyPromise = fetch(`https://api.jurassic-park.com/paddocks/${paddockId}/history`).then(r => r.json());
6
7 const [dinosaurs, status, history] = await Promise.all([
8 dinosaursPromise,
9 statusPromise,
10 historyPromise
11 ]);
12
13 return { paddockId, dinosaurs, status, history, updatedAt: new Date() };
14 } catch (error) {
15 console.error(`Error fetching data for paddock #${paddockId}:`, error);
16 throw error;
17 }
18}Fetch can handle various data formats, not just JSON:
1// Plain text
2fetch('https://api.jurassic-park.com/logs')
3 .then(response => response.text())
4 .then(text => {
5 console.log('Logs:', text);
6 document.getElementById('logs').textContent = text;
7 });
8
9// Binary data (images, files)
10fetch('https://api.jurassic-park.com/dinosaurs/42/image')
11 .then(response => response.blob())
12 .then(blob => {
13 const imageUrl = URL.createObjectURL(blob);
14 document.getElementById('dino-image').src = imageUrl;
15 });
16
17// Downloading and saving a file (in browser environment)
18fetch('https://api.jurassic-park.com/reports/monthly.pdf')
19 .then(response => response.blob())
20 .then(blob => {
21 const url = URL.createObjectURL(blob);
22 const a = document.createElement('a');
23 a.href = url;
24 a.download = 'monthly-report.pdf';
25 document.body.appendChild(a);
26 a.click();
27 document.body.removeChild(a);
28 URL.revokeObjectURL(url);
29 });Fetch can also handle form submissions:
1// Sending form data as JSON
2document.getElementById('dino-form').addEventListener('submit', async function(e) {
3 e.preventDefault();
4
5 const form = e.target;
6 const formData = new FormData(form);
7
8 // Convert FormData to a JavaScript object
9 const formObject = {};
10 for (const [key, value] of formData.entries()) {
11 formObject[key] = value;
12 }
13
14 try {
15 const response = await fetch('https://api.jurassic-park.com/dinosaurs', {
16 method: 'POST',
17 headers: {
18 'Content-Type': 'application/json'
19 },
20 body: JSON.stringify(formObject)
21 });
22
23 if (!response.ok) {
24 throw new Error(`HTTP Error: ${response.status}`);
25 }
26
27 const result = await response.json();
28 alert(`Dinosaur added successfully! ID: ${result.id}`);
29 form.reset();
30 } catch (error) {
31 console.error('Error submitting form:', error);
32 alert(`Error: ${error.message}`);
33 }
34});
35
36// Sending a form with file uploads
37document.getElementById('dino-upload-form').addEventListener('submit', async function(e) {
38 e.preventDefault();
39
40 const form = e.target;
41 const formData = new FormData(form);
42
43 try {
44 const response = await fetch('https://api.jurassic-park.com/dinosaurs/import', {
45 method: 'POST',
46 body: formData // FormData is automatically set as 'multipart/form-data'
47 });
48
49 if (!response.ok) {
50 throw new Error(`HTTP Error: ${response.status}`);
51 }
52
53 const result = await response.json();
54 alert(`Import complete! Added ${result.count} dinosaurs.`);
55 } catch (error) {
56 console.error('Error during import:', error);
57 alert(`Error: ${error.message}`);
58 }
59});CORS is a security mechanism that controls whether a page can make requests to resources on another domain:
1// Basic CORS request
2fetch('https://api.different-domain.com/data', {
3 mode: 'cors' // Default value
4});
5
6// Request with authentication
7fetch('https://api.different-domain.com/protected-data', {
8 mode: 'cors',
9 credentials: 'include' // Send cross-origin cookies
10});Credential options:
omit: Do not send cookies (default)same-origin: Send cookies only to the same domaininclude: Send cookies to other domains tooFetch lets you control how the browser uses its cache:
1// Ignore cache, always request from server
2fetch(url, { cache: 'no-store' });
3
4// Check cache first, then request if not found
5fetch(url, { cache: 'default' });
6
7// Check cache first but verify with server that the response is fresh
8fetch(url, { cache: 'no-cache' });
9
10// Use only cache, do not make a network request
11fetch(url, { cache: 'only-if-cached', mode: 'same-origin' });1async function apiRequest(endpoint, options = {}) {
2 const apiBaseUrl = 'https://api.jurassic-park.com';
3 const url = `${apiBaseUrl}${endpoint}`;
4
5 const defaultOptions = {
6 headers: {
7 'Content-Type': 'application/json',
8 'Authorization': `Bearer ${localStorage.getItem('apiToken')}`
9 },
10 mode: 'cors'
11 };
12
13 const mergedOptions = {
14 ...defaultOptions,
15 ...options,
16 headers: {
17 ...defaultOptions.headers,
18 ...(options.headers || {})
19 }
20 };
21
22 if (mergedOptions.body && typeof mergedOptions.body === 'object' && !(mergedOptions.body instanceof FormData)) {
23 mergedOptions.body = JSON.stringify(mergedOptions.body);
24 }
25
26 try {
27 const response = await fetch(url, mergedOptions);
28
29 if (response.status === 401) {
30 await refreshAuthToken();
31 return apiRequest(endpoint, options);
32 }
33
34 if (response.status === 204) {
35 return { success: true };
36 }
37
38 if (!response.ok) {
39 try {
40 const errorData = await response.json();
41 throw new Error(errorData.message || `HTTP Error: ${response.status}`);
42 } catch (e) {
43 throw new Error(`HTTP Error: ${response.status}`);
44 }
45 }
46
47 const contentType = response.headers.get('content-type');
48 if (contentType && contentType.includes('application/json')) {
49 return await response.json();
50 } else if (contentType && contentType.includes('text/')) {
51 return await response.text();
52 } else {
53 return await response.blob();
54 }
55 } catch (error) {
56 console.error(`API request error (${endpoint}):`, error);
57
58 const apiError = new Error(error.message);
59 apiError.isNetworkError = !window.navigator.onLine;
60 apiError.endpoint = endpoint;
61 apiError.originalError = error;
62
63 throw apiError;
64 }
65}1const apiCache = {
2 cache: new Map(),
3 ttl: 60000, // cache time-to-live in ms (1 minute)
4
5 async get(url, options = {}) {
6 const cacheKey = `${url}|${JSON.stringify(options)}`;
7 const cachedItem = this.cache.get(cacheKey);
8
9 if (cachedItem && Date.now() < cachedItem.expiry) {
10 console.log(`Using cache for: ${url}`);
11 return cachedItem.data;
12 }
13
14 console.log(`Making request for: ${url}`);
15 const response = await fetch(url, options);
16
17 if (!response.ok) {
18 throw new Error(`HTTP Error: ${response.status}`);
19 }
20
21 const data = await response.json();
22
23 if (!options.method || options.method === 'GET') {
24 this.cache.set(cacheKey, { data, expiry: Date.now() + this.ttl });
25 }
26
27 return data;
28 },
29
30 invalidate(urlPattern) {
31 for (const key of this.cache.keys()) {
32 if (key.includes(urlPattern)) {
33 this.cache.delete(key);
34 }
35 }
36 },
37
38 clear() { this.cache.clear(); }
39};1/**
2 * Executes a function with automatic retries
3 * @param {Function} fn - Function to execute that returns a Promise
4 * @param {Object} options - Options
5 * @param {number} options.maxRetries - Maximum number of retries
6 * @param {number} options.retryDelay - Delay before retry (ms)
7 * @param {Function} options.shouldRetry - Function determining whether to retry
8 * @param {number} options.backoffFactor - Delay multiplier (backoff)
9 * @returns {Promise} - Promise with the function result or the last error
10 */
11async function withRetry(fn, options = {}) {
12 const {
13 maxRetries = 3,
14 retryDelay = 1000,
15 shouldRetry = (error) => true,
16 backoffFactor = 2
17 } = options;
18
19 let lastError;
20
21 for (let attempt = 0; attempt <= maxRetries; attempt++) {
22 try {
23 return await fn();
24 } catch (error) {
25 lastError = error;
26
27 if (attempt === maxRetries || !shouldRetry(error)) {
28 break;
29 }
30
31 const delay = retryDelay * Math.pow(backoffFactor, attempt);
32 console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
33 await new Promise(resolve => setTimeout(resolve, delay));
34 }
35 }
36
37 throw lastError;
38}
39
40// Example usage with Fetch API
41async function fetchFromUnstableAPI() {
42 return withRetry(
43 () => fetch('https://api.jurassic-park.com/unstable-endpoint')
44 .then(response => {
45 if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
46 return response.json();
47 }),
48 {
49 maxRetries: 5,
50 retryDelay: 500,
51 backoffFactor: 1.5,
52 shouldRetry: (error) => {
53 // Only retry for 5xx errors (server errors) or network errors
54 return error.message.includes('HTTP Error: 5') ||
55 error instanceof TypeError;
56 }
57 }
58 );
59}The Fetch API is available in all modern browsers, and since version 18 it is also natively available in Node.js. However, there are some limitations and compatibility concerns:
No request progress support — Fetch has no built-in progress tracking for upload/download. For such cases, XMLHttpRequest may be a better option.
No native timeout — Timeouts must be implemented using AbortController.
Does not reject Promises for HTTP errors — Even if the server returns an error code, the Fetch Promise will resolve. You must check
response.ok.In older browsers, you may need to use a polyfill such as
whatwg-fetch or a library like axios that provides a consistent API across environments.Although the Fetch API is natively available, alternatives exist that may offer additional features:
Axios — A popular library that provides:
jQuery.ajax() — In older projects using jQuery
XMLHttpRequest — An older, more verbose alternative that offers more control, e.g., over request progress
Fetch is ideal when:
Consider alternatives when:
Always check response.ok
1fetch(url).then(response => {
2 if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
3 return response.json();
4});Set appropriate headers
1fetch(url, {
2 headers: {
3 'Content-Type': 'application/json',
4 'Accept': 'application/json'
5 }
6});Use AbortController for timeouts
1const controller = new AbortController();
2const timeout = setTimeout(() => controller.abort(), 5000);
3
4fetch(url, { signal: controller.signal })
5 .then(response => /* ... */)
6 .catch(error => /* ... */)
7 .finally(() => clearTimeout(timeout));Be careful with body processing
1// WRONG — attempting to use body multiple times
2fetch(url)
3 .then(response => {
4 const text = response.text(); // This consumes the body!
5 const json = response.json(); // Error — body already consumed!
6 return /* ... */;
7 });
8
9// CORRECT — clone response if you need to use it multiple times
10fetch(url)
11 .then(response => {
12 const clone = response.clone();
13 return response.text().then(text => {
14 return clone.json().then(json => {
15 return { text, json };
16 });
17 });
18 });Use async/await for readability
1async function fetchData() {
2 try {
3 const response = await fetch(url);
4 if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
5 const data = await response.json();
6 return data;
7 } catch (error) {
8 console.error('Error:', error);
9 throw error;
10 }
11}The Fetch API is a powerful, built-in tool for making HTTP requests in JavaScript. Its main advantages are the Promise-based API, ease of use, and versatility. Although it has some limitations compared to more advanced libraries like Axios, for most use cases it is completely sufficient.
In Jurassic Park, where reliable communication between different systems is key to the safety of visitors and dinosaurs, the Fetch API enables reliable data exchange between monitoring systems, databases, and user interfaces.
In the next exercise we will look at how to combine the Fetch API with other asynchronous techniques to create even more advanced systems for managing our digital Jurassic Park.