Learning path · Level 7 · Tutorial 2

Fetch and Validate JSON Responses

Retrieve JSON with fetch, check the HTTP result, and verify the fields your interface needs before using the data. Practice with JSON files you create locally from text included in the tutorial and serve through Vite.

  • Level: Intermediate
  • Duration: 25 min
  • 7 steps
Start tutorial Back to the path
Fetch and Validate JSON Responses

What you will go through

  1. Fetch a Local JSON Resource Request a product fixture through Vite and distinguish the returned Response from the product data it will later provide. 3 min
  2. Build Query Strings Safely Create a GET request URL with URL and URLSearchParams so spaces and special characters remain part of their intended query values. 3 min
  3. Check the HTTP Result First Reject an unsuccessful HTTP response before processing its body. 3 min
  4. Decode the Body as JSON Decode a successful response body once, use headers and the Network panel as diagnostic evidence, and distinguish decoding from HTTP failures. 3 min
  5. Validate the Records You Need Validate decoded product data at the boundary before other code accesses its fields. 5 min
  6. Diagnose Responses in the Network Panel Inspect local fixture requests in the Network panel and use observed status, headers, and body evidence to identify the first response gate that rejects a result. 4 min
  7. Apply the Complete Fetch-and-Validate Flow Assemble and verify a local browser JavaScript loader that returns only validated product records. 5 min

What you will learn

  • Build a GET request URL with correctly encoded query parameters.
  • Check an HTTP response before attempting to use its body.
  • Decode JSON and reject data whose structure does not match the expected records.
  • Use the Network panel to distinguish HTTP failures, non-JSON responses, and invalid record shapes.

Before you start

  • Sequence Asynchronous Work with async/await
  • Represent Records and Lists with Objects and Arrays
  • Transform Arrays with Callback Functions
  • Run a Local Development Server with Vite

Step 1 of 7

Fetch a Local JSON Resource

Request a product fixture through Vite and distinguish the returned Response from the product data it will later provide.

Prepare a local product resource

Create the practice files

Use your existing Vite project and keep its development server running. Create the two files below exactly at these locations. The JSON file is a local fixture: it is data for the browser to request, not JavaScript to import.

Root-level index.html

index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Product catalog</title>
  </head>
  <body>
    <h1>Product catalog</h1>
    <script type="module" src="/main.js"></script>
  </body>
</html>

Product fixture

public/data/products.json

json
[
  {"id":1,"name":"Notebook","inStock":true},
  {"id":2,"name":"Pen","inStock":false}
]

Make the default GET request

Follow the resource request

Vite serves files inside public from the server root. Therefore, the file stored as public/data/products.json has the browser URL /data/products.json—not /public/data/products.json.

With no options, fetch(path) makes a GET request. Awaiting it gives the request result as a Response; it does not give the product array directly.

From file to Response

A four-stage flow from a JSON file in the public data folder to its root-relative browser URL, a browser request, and a Response object.

The local file and its HTTP URL are different things: Vite maps the public folder to the root URL path. The browser request returns a Response object.

Request the fixture

main.js

javascript
async function loadProducts(path, query) {
  const response = await fetch(path);
  console.log(response.constructor.name);
}

loadProducts("/data/products.json", {});
Expected output
Response

The browser's console formatting can differ, but the logged value should identify the returned object as Response.

Tip

Keep the paths separate

Do not open the JSON file with a file: URL. Open the localhost address reported by Vite, then use the browser console to see the result. The second function parameter will be used later; this first request has no query parameters.

Apply the distinction

Check your mental model

Complete both blanks. The first must make the request, and the second must identify the kind of value held in response.

Complete the request

Fill in the missing code.

javascript
async function loadProducts(path, query) {
  const response = await (path);
  // `response` is a , not the product array.
  console.log(response);
}

loadProducts("/data/products.json", {});

Step 2 of 7

Build Query Strings Safely

Create a GET request URL with URL and URLSearchParams so spaces and special characters remain part of their intended query values.

Build the base resource URL

Start with an origin and a resource path

A URL has two useful parts here: the origin of the page Vite is serving and the root-relative resource path. Build a URL object instead of joining strings.

For a Vite project, the file at public/data/products.json is requested as /data/products.json. It is not requested as /public/data/products.json.

new URL(path, window.location.origin) combines the current local origin with that resource path. The resulting URL can be passed to fetch, which makes a GET request by default.

How the URL is assembled

Diagram showing an origin and root-relative resource path combining into a URL object, with a separate area reserved for a query string.

Create the URL from the page origin and /data/products.json; add the query string to that URL object afterward.

Create the base URL

This code creates the base URL. It does not request the file yet.

javascript
const path = "/data/products.json";
const url = new URL(path, window.location.origin);

Attach and inspect the query

Serialize raw query values once

Keep query values in their raw form. URLSearchParams serializes them safely, then its result becomes url.search.

For the raw value "notebook & pen", the space becomes + and the ampersand inside the value becomes %26. The final & before limit=2 is different: it separates two parameters.

Do not manually concatenate raw values into a query string. Also do not pre-encode values before passing them to URLSearchParams; it performs the encoding itself.

Attach a safe query string

Place this temporary snippet in main.js and view the browser console while Vite is running.

javascript
const path = "/data/products.json";
const query = {
  q: "notebook & pen",
  limit: "2"
};
const url = new URL(path, window.location.origin);

url.search = new URLSearchParams(query).toString();

console.log(url.toString());
Expected output
http://localhost:5173/data/products.json?q=notebook+%26+pen&limit=2

Your Vite port may differ. The important search portion is `?q=notebook+%26+pen&limit=2`.

Tip

Encoding is not server-side filtering

The query string changes the request URL, but it does not turn the static fixture into a search API. Vite still serves the same /data/products.json file for this example, regardless of q and limit.

What each encoded part means

Read the serialized query from left to right.

Only the unescaped ampersand separates parameters.
PartMeaning
q=Start of the q parameter
notebook+%26+penOne q value: notebook & pen
&limit=2A second parameter named limit

Build a safely encoded URL

Practice: construct and serialize the URL

Complete the URL construction so the raw search value remains one q value when serialized.

javascript
const query = {
  q: "notebook & pen",
  limit: "2"
};

const url = new ("/data/products.json", );
url.search = (query).toString();

console.log(url.search);

Step 3 of 7

Check the HTTP Result First

Reject an unsuccessful HTTP response before processing its body.

Separate received responses from fetch rejections

A response is not automatically a success

fetch() can end in two importantly different ways. If the browser receives an HTTP response, await fetch(...) gives you a Response—even when its status is an error such as 404. That received response has a numeric response.status and an response.ok value.

response.ok is true only for status codes from 200 through 299. Check it before doing anything with the body.

A fetch rejection is different: await fetch(...) throws before there is a Response to inspect. The HTTP guard is not reached in that route.

Where the HTTP gate belongs

A flow diagram showing that both a 200 and a 404 received as Responses pass through an HTTP status gate, while a fetch rejection bypasses that gate because no Response exists.

A 404 is a received Response, so the loader can inspect it. A fetch rejection has no readable Response in this flow.

Reject unsuccessful responses explicitly

Read status, then enforce the gate

Use status when reporting the result, and use ok for the compact success check. The guard belongs immediately after await fetch(url) and before any body-processing code.

Status comparison

These two outcomes both provide a Response, but only one is allowed past the HTTP gate.

A received HTTP error must be rejected explicitly by application code.
Observed statusresponse.okfetch resultAction before body use
200trueFulfills with ResponseContinue past the gate
404falseFulfills with ResponseThrow HTTP 404

Add an explicit HTTP guard

This HTTP stage returns only a response that passed the status check. Body decoding belongs after this gate.

javascript
async function loadProducts(path, query) {
  const url = new URL(path, window.location.origin);
  url.search = new URLSearchParams(query).toString();

  const response = await fetch(url);

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

  return response;
}

Practice: follow a received 404

Predict the 404 route

Assume fetch() fulfills with a Response whose status is 404. What happens in a loader with an if (!response.ok) guard immediately after await fetch(...)?

Apply the guard

Complete the HTTP guard

Complete the HTTP gate. Assume fetch has fulfilled with a Response whose status is 404; this condition must route that received response to the thrown HTTP error.

javascript
async function loadProducts(path, query) {
  const url = new URL(path, window.location.origin);
  url.search = new URLSearchParams(query).toString();

  const response = await fetch(url);

  if () {
    throw new Error(`HTTP ${response.status}`);
  }

  return response;
}

Step 4 of 7

Decode the Body as JSON

Decode a successful response body once, use headers and the Network panel as diagnostic evidence, and distinguish decoding from HTTP failures.

Parse only after the HTTP gate

Decode after the HTTP check

After the HTTP gate passes, decode the body with await response.json(). This asynchronous method reads the response body and parses JSON text into a JavaScript value.

The value is stored in data because it has been decoded but is not yet trusted. Valid JSON syntax does not prove that its records contain the fields your interface needs; that boundary check comes next.

Read the body only after response.ok is true. A received response, a decoded body, and accepted product records are separate stages.

Diagram showing the HTTP gate before a response body is parsed once into JavaScript data, with separate failure paths for an unsuccessful status and malformed content.

The HTTP check happens before the one-time body read. Parsing produces a JavaScript value, not yet validated product records.

Read the successful body

Add this immediately after the established response.ok guard.

javascript
const data = await response.json();

Use headers and the Network panel for diagnosis

Treat Content-Type as a clue

A response header can provide a useful clue while diagnosing a result:

const contentType = response.headers.get("content-type");

For example, a JSON resource commonly reports a value containing application/json, while an HTML document commonly reports text/html. But this is evidence, not proof. A JSON-looking header cannot guarantee valid JSON syntax or the product fields your interface expects.

Likewise, response.json() attempts JSON decoding regardless of the declared content type. If an HTML body arrives where JSON is expected, decoding still fails.

Warning

Read the body once

A response body is a one-time resource. Do not add response.text() to inspect it and then call response.json() on the same response; the second read cannot use the already consumed body.

Instead, open the browser Network panel, select the request, and inspect its Response or Preview body there. This lets you compare the actual body with the decoding error without competing with your application code.

Practice one awaited body read

Practice the JSON read

Assume the HTTP guard has already accepted this response. Complete the single method call that asynchronously reads and parses its body.

Complete the body-reading method.

javascript
const data = await response.();

Classify response evidence

Locate the first failing stage

Match each observed response to its first result in the fetch-and-validate flow.

Tap an item and then its matching pair.

Step 5 of 7

Validate the Records You Need

Validate decoded product data at the boundary before other code accesses its fields.

The product data contract

Define the boundary contract

Decoded JSON is still untrusted at the boundary. Before other code reads product fields, require a top-level array. Every record in that array must have:

  • id: a number
  • name: a string
  • inStock: a boolean

This is a structure contract, not a business-rule check. An empty array is valid. Extra fields are valid. inStock: false is valid because false is a boolean value, not a missing value.

If any record has an unexpected structure, reject the whole result instead of filtering, coercing, or partially using it.

Validate the collection and its records

A diagram shows an array and each product record passing through a validation gate, while null, arrays, missing fields, and wrong field types are rejected.

Validate the collection first, then validate every candidate record.

Contract examples

The contract intentionally allows valid values that might otherwise look unusual.

Accepted data can have false values, extra fields, or no records.
Input caseResultWhy
Empty arrayAcceptIt satisfies the collection contract.
inStock is falseAcceptFalse has the required boolean type.
Additional price fieldAcceptExtra fields are allowed.
id is a stringRejectid must have number type.
Null recordRejectA record must be a non-null object.

Validate one record, then all records

Guard before reading fields

Use one helper for a candidate record, then apply it to the complete collection. JavaScript stops an && chain as soon as a condition fails. Put the object guards before field reads, so property access happens only for a non-null object that is not an array.

Check Array.isArray(data) before calling data.every(...); a non-array has no applicable collection to validate this way. every returns true for an empty array, which correctly accepts an empty product list.

Validation helpers

Add these helpers after decoding the body. Only the returned value has passed the product contract.

javascript
function isProduct(record) {
  return (
    typeof record === "object" &&
    record !== null &&
    !Array.isArray(record) &&
    typeof record.id === "number" &&
    typeof record.name === "string" &&
    typeof record.inStock === "boolean"
  );
}

function validateProducts(data) {
  if (!Array.isArray(data) || !data.every(isProduct)) {
    throw new Error("Invalid product data");
  }

  return data;
}

const products = validateProducts(data);

Tip

Why type checks matter

Do not use truthiness for required fields. For example, if (record.inStock) would reject the valid value false. A typeof check verifies the required kind of value instead. A missing field evaluates as undefined, so it also fails the expected type check.

Quick check

Check an allowed edge case

True or false: Under this product contract, validateProducts([]) should return the empty array rather than throw Invalid product data.

Practice: complete the validator

Build the validator

Complete the validator so it accepts only the required product structure. It must reject a non-array collection, null records, arrays used as records, missing fields, and incorrect field types. It must still allow an empty array and a boolean value of false.

Complete the validation boundary

Complete all six guards.

javascript
function isProduct(record) {
  return (
     &&
     &&
     &&
     &&
    
  );
}

function validateProducts(data) {
  if (! || !data.every(isProduct)) {
    throw new Error("Invalid product data");
  }

  return data;
}

Step 6 of 7

Diagnose Responses in the Network Panel

Inspect local fixture requests in the Network panel and use observed status, headers, and body evidence to identify the first response gate that rejects a result.

Capture evidence before diagnosing

Collect request evidence

Open the browser DevTools Network panel before running a request. Reload the page or run the loader, then select the matching resource request. Inspect:

  1. Request URL — the path and query actually sent.
  2. Request Method — this loader uses GET.
  3. Status — the numeric HTTP result.
  4. Response Headers — especially content-type.
  5. Response or Preview — the body actually returned.

Use this evidence to identify the first rejected gate. An unsuccessful status is an HTTP failure. A successful response whose body cannot be parsed is a JSON decoding failure. Valid JSON with records that break the product contract is a validation failure. A caught error alone does not tell you which category occurred.

Where to inspect a request

A selected Network-panel request connected to visual areas for its URL, method, status, headers, and response body.

Select the matching request and inspect its URL, method, status, headers, and body as evidence.

Prepare comparable local responses

Create diagnostic fixtures

Create these three files in public/data/. Their bodies deliberately represent three different diagnostic cases. Do not create /data/missing.json; it remains intentionally absent.

After creating the files, you will use one complete local loader and change only its path value for each request. The filename is not the diagnosis: inspect the returned status, headers, and body in the Network panel.

Malformed JSON fixture

Create public/data/products-malformed.json.

json
[
  {"id":1,"name":"Notebook","inStock":true},
  {"id":2,"name":"Pen","inStock":false},
]

Wrong record shape fixture

Create public/data/products-invalid.json.

json
[
  {"id":"1","name":"Notebook","inStock":true},
  {"id":2,"name":"Pen","inStock":false}
]

HTML instead of JSON fixture

Create public/data/not-json.html.

html
<!doctype html>
<html>
  <body>
    <p>Not JSON</p>
  </body>
</html>

Request each local fixture

Run one complete diagnostic loader

Replace your root-level main.js with this diagnostic loader. It includes every gate: URL construction, HTTP checking, one JSON read, record validation, and a calling boundary that reports caught failures.

Start with the successful path. Then change only the path value near the bottom to each diagnostic path, reload, and inspect its Network entry. Keep the Network panel open while you do this.

Complete local diagnostic loader

Root-level main.js. Change only the path value between runs.

javascript
function isProduct(record) {
  return (
    typeof record === "object" &&
    record !== null &&
    !Array.isArray(record) &&
    typeof record.id === "number" &&
    typeof record.name === "string" &&
    typeof record.inStock === "boolean"
  );
}

function validateProducts(data) {
  if (!Array.isArray(data) || !data.every(isProduct)) {
    throw new Error("Invalid product data");
  }

  return data;
}

async function loadProducts(path, query) {
  const url = new URL(path, window.location.origin);
  url.search = new URLSearchParams(query).toString();

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const data = await response.json();
  const products = validateProducts(data);
  return products;
}

async function start() {
  const path = "/data/products.json";

  try {
    const products = await loadProducts(path, {
      q: "notebook & pen",
      limit: "2"
    });
    console.log(products);
  } catch (error) {
    console.error(error.message);
  }
}

start();
Expected output
[
  { id: 1, name: "Notebook", inStock: true },
  { id: 2, name: "Pen", inStock: false }
]

Browser consoles format logged arrays and objects differently. The important result is an accepted array containing Notebook and Pen; error wording from JSON decoding can vary by browser.

Tip

Change one path at a time

Use these values for path, one run at a time:

  • /data/products-malformed.json
  • /data/products-invalid.json
  • /data/not-json.html
  • /data/missing.json

For the missing path, do not predict the result from the name. In a Vite application, an absent path can produce an HTML fallback with a successful status. The observed status determines whether the HTTP gate stops first; an accepted status can allow the HTML body to reach JSON decoding.

Compare evidence and gates

Locate the first rejected gate

Read the body before naming the failure. content-type is useful evidence, but it does not prove JSON syntax or validate record fields. The first rejected gate is the diagnosis; do not describe every later problem that might have occurred.

The last row is a supplied Network reference, not a result you need Vite to produce locally. It ensures you can compare your local observations with an actual unsuccessful HTTP status.

Evidence patterns

Use observed evidence for the missing path rather than assuming it is a 404.

A body can distinguish invalid JSON syntax from valid JSON that has an invalid product record.
RequestTypical Network evidenceFirst rejected gateBody clue
/data/products-malformed.jsonSuccessful status; JSON content typeJSON decoding failureTrailing comma
/data/products-invalid.jsonSuccessful status; JSON content typeValidation failureid is a string
/data/not-json.htmlSuccessful status; HTML content typeJSON decoding failureHTML document
/data/missing.jsonObserve actual status and bodyDepends on evidencePossible HTML fallback
Supplied 404 reference404; HTML content typeHTTP failureNot Found response

Reference: an actual HTTP 404

A Network-panel reference with a GET request, status 404, an HTML response header, and an HTML response body.

A status of 404 is an HTTP failure. The HTTP gate stops processing before the body needs JSON decoding.

Practice classifying evidence

Classify the first failing gate

Match each Network evidence pattern to the first failure category.

Tap an item and then its matching pair.

Apply the evidence checklist

Report one observed local failure

Choose one local failing request you ran: the malformed JSON fixture, invalid-record fixture, HTML fixture, or missing path. From its selected Network entry, classify the evidence by writing four labeled lines:

  • status: the observed integer
  • content-type: the observed header value, or none
  • body clue: a short observation
  • first failure: HTTP failure, JSON decoding failure, or validation failure

Your classification must be based on the request evidence, not the filename.

Write at least 70 characters (0/70).

Step 7 of 7

Apply the Complete Fetch-and-Validate Flow

Assemble and verify a local browser JavaScript loader that returns only validated product records.

Connect the established stages

One protected path to usable products

A fulfilled fetch() promise means only that the browser received a Response. Your caller should receive products only after the URL is built, the HTTP result is accepted, the body is decoded once, and the decoded value passes the product contract.

Continue with the local Vite project and fixtures from earlier steps. This loader makes a GET request; it adds no loading UI, retry behavior, or request cancellation. One try/catch at the calling boundary handles every thrown failure.

The loader pipeline

A pipeline from a request URL through a Response, an HTTP check, JSON decoding, record validation, and accepted product records in a console.

A Response is an intermediate result. The caller receives records only after the validation checkpoint.

Tip

Observe both sides

Keep the Network panel open alongside the console while you run the cases. The request URL can contain the encoded search string, but Vite serves the same static fixture regardless of these example query values.

Run the integrated local implementation

A complete main.js

Replace the contents of root-level main.js with this complete loader. Your existing index.html continues to load /main.js. The successful run requests /data/products.json, then logs both the accepted records and their names at the calling boundary.

javascript
function isProduct(record) {
  return (
    typeof record === "object" &&
    record !== null &&
    !Array.isArray(record) &&
    typeof record.id === "number" &&
    typeof record.name === "string" &&
    typeof record.inStock === "boolean"
  );
}

function validateProducts(data) {
  if (!Array.isArray(data) || !data.every(isProduct)) {
    throw new Error("Invalid product data");
  }

  return data;
}

async function loadProducts(path, query) {
  const url = new URL(path, window.location.origin);
  url.search = new URLSearchParams(query).toString();

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const data = await response.json();
  const products = validateProducts(data);
  return products;
}

async function start() {
  const path = "/data/products.json";

  try {
    const products = await loadProducts(path, {
      q: "notebook & pen",
      limit: "2"
    });
    console.log(products);
    console.log(products.map((product) => product.name));
  } catch (error) {
    console.error(error.message);
  }
}

start();
Expected output
[
  { id: 1, name: "Notebook", inStock: true },
  { id: 2, name: "Pen", inStock: false }
]
["Notebook", "Pen"]

Browser developer tools may format logged objects differently. The important result is an accepted array of two records, followed by the two product names.

Verification guide

Change only the path value in start(), reload with the Network panel open, and inspect the matching request. Diagnose from its observed status and body—not from the filename.

Expected first result for each case
Request caseNetwork evidenceFirst failureConsole result
`/data/products.json`200; valid product arrayNo failureAccepted products and names
`/data/products-malformed.json`200; trailing comma in bodyJSON decoding failureNative JSON error
`/data/products-invalid.json`200; id is a stringvalidation failureInvalid product data
`/data/not-json.html`200; HTML document bodyJSON decoding failureNative JSON error
Supplied 404 example404 statusHTTP failureHTTP 404

Summary

Request-to-validation checklist

Follow this checklist on every run. Receiving a response, parsing its body, and accepting its records are distinct outcomes. For a missing path under Vite, use observed Network panel evidence rather than assuming the result is a 404.

  • Build the URL from the resource path and raw query values.
  • Receive a Response, then check the HTTP result before reading its body.
  • Decode the body once as JSON, then validate the array and every record.
  • Return products only after all gates pass; catch failures at the caller.

Practice the implementation

Complete the protected loader

Complete the four expressions so an unsuccessful HTTP result stops before decoding and only validated data is returned.

javascript
async function loadProducts(path, query) {
  const url = new URL(path, window.location.origin);
  url.search = new URLSearchParams(query).toString();

  const response = await fetch(url);
  if (!) {
    throw new Error(`HTTP ${response.}`);
  }

  const data = await response.();
  const products = (data);
  return products;
}

Verify the processing order

Order the processing gates

Put the stages of one loader run in execution order.

  1. Decode the body as JSON
  2. Reject an unsuccessful HTTP result
  3. Build the request URL with the query string
  4. Validate data and return products to the caller
  5. Await fetch and receive a Response

Final application and review

Report evidence from the complete flow

Using the completed loader, run /data/products.json, then replace only its path with /data/products-malformed.json, /data/products-invalid.json, and /data/not-json.html.

Submit one concise report that includes:

  • the product names that reach the caller for the successful fixture;
  • the ordered processing gates;
  • the first failing stage for each of the three failing fixtures, with one Network panel status or body clue for each;
  • the first failing stage and console message for the supplied Network example whose status is 404.

Classify failures as HTTP failure, JSON decoding failure, or validation failure.

Write at least 180 characters (0/180).

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate