Learning path · Level 7 · Tutorial 3

Send JSON Data with HTTP Requests

Construct JSON write requests and handle their response contracts without assuming that sending data means it was saved. Inspect outgoing requests locally, using response examples to interpret successful writes and recognizing that the Vite server does not provide a writable API.

  • Level: Intermediate
  • Duration: 20 min
  • 6 steps
Start tutorial Back to the path
Send JSON Data with HTTP Requests

Step 1 of 6

Match the Method to the Operation

Choose POST, PUT, PATCH, DELETE, or GET from the documented task operation and determine when a request body belongs.

Choose from the API contract

Read the documented contract

An HTTP method follows the API’s documented operation—not a universal guess. For this illustrative task API, the collection is /api/tasks and task 7 is /api/tasks/7.

A task’s writable fields are title and completed. The target URL identifies an existing task; it is not part of the writable task representation.

Task API operations

Use this contract to choose both the method and whether a request body is expected. These are illustrative API rules; another API can document different rules.

Illustrative task API request contract
OperationEndpointMethodExpected request body
Create a task/api/tasksPOSTFull writable task
Replace task 7 fields/api/tasks/7PUTFull writable task
Update selected task 7 fields/api/tasks/7PATCHOnly supplied fields
Delete task 7/api/tasks/7DELETENo request body

Collection and individual task operations

A diagram distinguishes the task collection from one selected task, with icons for creating, replacing, partially changing, deleting, and filtering.

The collection path addresses many tasks; /api/tasks/7 addresses one existing task.

Decide what the request carries

Replacement versus partial update

PUT replaces writable fields. For task 7, the documented PUT operation supplies the complete writable representation, such as:

{ "title": "Practice HTTP requests", "completed": false }

PATCH updates only supplied fields. To mark that task complete, its partial-update representation is:

{ "completed": true }

Under this contract, PATCH leaves writable fields that are not supplied unchanged. This is a plain partial representation, not a patch-document format.

Two body strategies

A comparison shows a complete task card replacing another card on the left and a single completion field changing on the right.

PUT supplies the complete writable representation; PATCH supplies only the fields to change.

When no request body belongs

A DELETE request targets the task to remove and has no request body in this contract.

GET is different: put filters in the URL, for example /api/tasks?completed=false, rather than supplying a request body. The URL describes which tasks to retrieve.

Tip

Do not infer a method from an endpoint name alone. First identify the documented operation: create, replace, update selected fields, delete, or retrieve filtered results.

Practice method and body choices

Apply the contract

Match each documented task operation to its correct method and body strategy.

Tap an item and then its matching pair.

Step 2 of 6

Build a JSON Request

Serialize a task object and configure the method, headers, and body of a JSON POST request.

Turn the object into request data

Serialize the task

For the documented create operation, the request body must be JSON text—not a JavaScript object. Start with the task object, then call JSON.stringify(task) before placing the result in fetch().

The resulting request body is:

{"title":"Practice HTTP requests","completed":false}

It looks similar to the object, but it is text with JSON syntax: property names use double quotes, and there are no JavaScript variable names or object behavior attached.

Object to JSON text

The source object stays available in JavaScript; requestBody holds its JSON representation.

javascript
const task = { title: 'Practice HTTP requests', completed: false };
const requestBody = JSON.stringify(task);

From object to HTTP request

A diagram showing a JavaScript task object transformed by JSON.stringify into JSON text, then placed in the body of a POST request with JSON-related headers.

JSON.stringify() transforms the source object into the JSON text sent as the request body.

Configure the JSON POST request

Assign each request part

A create request combines the documented method with three options:

  • method states the operation: POST for this task creation contract.
  • headers describe what is being sent and what format is preferred in return.
  • body contains the serialized JSON text.

Content-Type describes the outgoing request body. Accept expresses a preference for the response format. Neither header serializes task, and Accept does not guarantee what a server will return.

Header roles

Use these two JSON headers for the illustrative task API.

Both headers use the media type `application/json`, but they describe different directions of communication.
PartMeaning
Content-TypeThe request body is JSON
AcceptJSON is the preferred response format
bodySerialized JSON text from task
methodPOST creates a task

POST a serialized task

This function constructs the documented task-creation request. Response interpretation comes in a later step.

javascript
const task = { title: 'Practice HTTP requests', completed: false };

async function createTask() {
  const response = await fetch('/api/tasks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify(task)
  });

  return response;
}

Tip

Keep request construction separate from confirmation

Do not treat a configured request as proof that a task was stored. The method, headers, and body describe what the browser will send; a later response check determines whether the API confirmed the write.

Practice: construct the JSON request

Complete the request options

Complete this documented task-creation request.

javascript
const response = await fetch('/api/tasks', {
  method: '',
  headers: {
    '': '',
    '': ''
  },
  body: 
});

Step 3 of 6

Inspect a Write Request in Vite

Send a JSON POST request from a Vite project, inspect it in DevTools, and distinguish an outgoing request from a confirmed API write.

Separate request evidence from write confirmation

Sending is not saving

A request appearing in the browser is evidence that the browser attempted to send it. It is not evidence that a server accepted or stored a task.

In this local project, /api/tasks is an illustrative API path, not an API implemented by Vite. You can still inspect the outgoing request in DevTools and verify that its method, headers, and request body are correct.

Warning

Local-server boundary

Vite serves development resources; it does not turn /api/tasks into a writable task service. Likewise, sending POST to a static JSON resource would not edit that file. Record the response you actually receive—do not assume a particular status, fallback, or response format.

Send one request from the button

Create a manually triggered request

Use these complete files in your existing Vite project. Save them, run npm run dev, open the address reported by Vite, and open DevTools before clicking the button. The page intentionally has no status interface: inspect the request in DevTools instead.

index.html

Replace your project entry document with this page.

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Inspect a task request</title>
  </head>
  <body>
    <main>
      <h1>Task request inspector</h1>
      <button id="send-request" type="button">Send task request</button>
    </main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
Expected output
Rendered page:

Task request inspector
[Send task request]

This is the stable rendered result before any request is sent. Clicking the button sends one request; no page status is rendered.

src/main.js

Place this browser module in src/main.js. It logs the actual local response facts after one click.

javascript
const task = { title: 'Practice HTTP requests', completed: false };

const sendButton = document.querySelector('#send-request');

if (!sendButton) {
  throw new Error('Missing #send-request button.');
}

sendButton.addEventListener('click', async () => {
  const response = await fetch('/api/tasks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify(task)
  });

  console.log('Observed Vite response', {
    status: response.status,
    contentType: response.headers.get('content-type')
  });
});
Expected output
After clicking Send task request once, the Console output has this shape:

Observed Vite response { status: <locally observed integer>, contentType: <locally observed header value or null> }

The status and content type are deliberately not fixed: Vite behavior can vary. After clicking once, use the actual integer and header value shown in DevTools Network as your Observed Vite response.

Read Network without inventing a success

Inspect the actual exchange

In DevTools, open Network, click Send task request once, and select the request whose URL ends in /api/tasks.

  1. In Headers, record the Request URL and Request Method.
  2. Under Request Headers, record Content-Type. Under Response Headers, record content-type if it is present.
  3. Open Payload and record the JSON request body.
  4. Record the actual Status Code shown for this request.

Label the status and response content type Observed Vite response. The method, request header, and payload show what the browser sent. They do not show that a task was stored.

Where to look in Network

Conceptual DevTools Network panel with highlighted areas for the request URL, method, request and response headers, JSON payload, and response status.

Use Network, Headers, and Payload to keep evidence about the outgoing request separate from the response returned by Vite.

Warning

Interpret the evidence carefully

A correctly formed outgoing POST proves only that browser code created and sent that request. A write can be confirmed only when an API responds according to its documented contract. This project has no implemented task API, so an Observed Vite response is not proof that a task was saved.

Practice: evaluate the local evidence

Report your local observation

After clicking the button once, write a compact verification note. Include:

  • the observed method;
  • the request Content-Type;
  • the exact JSON payload;
  • the response status and response content-type you recorded as the Observed Vite response; and
  • why these observations show that the browser sent a request but do not show that Vite saved a task.

Write at least 160 characters (0/160).

Step 4 of 6

Keep Rejected Writes Out of the Success Path

Use an HTTP-status gate so rejected write responses never reach JSON decoding or a success confirmation.

Stop failures early

Gate the success path

A write request can leave the browser and still be rejected by the server. Treat response.ok as a gate: only a documented successful HTTP response may continue toward decoding and confirmation.

Check that gate before reading a success body. Rejected responses may contain HTML, plain text, or no body at all. Trying response.json() first can hide the useful fact that the HTTP write was rejected.

For this task contract, a successful POST /api/tasks returns 201 Created with JSON. A response with ok === false must stop the success path.

Response gate flow

A flow diagram where rejected HTTP responses stop before JSON decoding and confirmation, while successful responses continue.

Check the HTTP result first; decode and confirm only on the successful branch.

Put confirmation after handling

Use status, then handle uncertainty

Use a small status-based error when the API explicitly rejects the write. The error does not depend on a particular error-body format.

The catch also handles an unavailable response or a body that cannot be decoded. In either case, the outcome is unconfirmed—do not claim that remote work was undone or saved.

Reject before decoding

A focused creation function for the documented JSON response contract.

javascript
const task = { title: 'Practice HTTP requests', completed: false };

async function createTask() {
  try {
    const response = await fetch('/api/tasks', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(task)
    });

    if (!response.ok) {
      throw new Error('Write rejected with status ' + response.status);
    }

    const data = await response.json();
    console.log(data);
    console.log('Write confirmed by the API.');
  } catch (error) {
    console.error('Write outcome is unconfirmed.', error);
  }
}

Name only what the evidence supports

Describe the outcome accurately

Two non-success situations require different statements. Neither belongs on the confirmation path.

Rejected versus unconfirmed

Use the evidence that is actually available.

A missing confirmation is not proof that a remote change was reversed.
SituationEvidence availableCorrect handlingWhat not to claim
HTTP rejectionA Response exists; `response.ok` is false and status is availableThrow an error using the status before decoding a success bodyThat the write succeeded
Unavailable or unreadable outcomeNo usable successful result; fetch may reject or JSON decoding may failCatch the failure and report an unconfirmed outcomeThat the remote work was undone
Observed Vite responseA local development-server response, not a task API confirmationInspect it in DevTools and keep it separate from the illustrative contractThat Vite stored a task

Practice: add the gate

Protect the confirmation path

Complete the rejection guard so an illustrative rejected write cannot be decoded as a success or produce a success confirmation.

javascript
const response = await fetch('/api/tasks', options);

if () {
  throw new Error('Write rejected with status ' + response.status);
}

const data = await response.json();
console.log(data);
console.log('Write confirmed by the API.');

Step 5 of 6

Handle JSON and 204 Responses

Implement operation-specific response validation and safely handle documented JSON and no-content write responses.

Map the contract to response paths

Let the operation define success

A 2xx status is not enough on its own. The endpoint operation determines which successful status can confirm that write. Check that operation-specific status before passing the response to the body helper.

For example, a 201 Created response is correct for the documented create operation, but it is not a documented success for PATCH /api/tasks/7. Even valid-looking JSON with that unexpected status must not confirm the PATCH write.

After the caller accepts the operation's status, readWriteResponse(response) handles the response body: it returns a validated task record for JSON or null for permitted no content.

Documented write outcome contract

The documented status and body depend on the task operation.

Validate the status for the specific operation before reading its response body.
OperationEndpointPermitted successBody result
Create/api/tasks201 CreatedTask JSON
Replace/api/tasks/7200 OK or 204 No ContentTask JSON or no body
Partial update/api/tasks/7200 OK or 204 No ContentTask JSON or no body
Delete/api/tasks/7204 No ContentNo body

Validate status before choosing a body path

A PATCH response flow where documented statuses lead to either a task record or an empty result, while an unexpected success status, rejection, and invalid content are stopped.

For PATCH, accept only its documented statuses. A permitted no-content response exits before JSON decoding.

Implement the two-stage response check

Guard the operation, then read the body

Keep two responsibilities separate. The PATCH caller first rejects any status other than 200 OK or 204 No Content. Only then does it call readWriteResponse(response).

The helper keeps the earlier rejection guard as a defensive check. It returns null immediately for 204 No Content. For a permitted JSON path, it decodes data and validates the familiar id, title, and completed fields. An HTML fallback or malformed JSON makes response.json() fail; an incorrect record shape also throws. Both outcomes remain unconfirmed.

PATCH status guard and response helper

The status guard belongs to the PATCH caller. The helper is only reached after that guard accepts the documented PATCH status.

javascript
function isTask(value) {
  return value !== null &&
    typeof value === 'object' &&
    typeof value.id === 'number' &&
    typeof value.title === 'string' &&
    typeof value.completed === 'boolean';
}

function assertPatchResponseStatus(response) {
  if (!response.ok) {
    throw new Error('Write request failed: ' + response.status);
  }

  if (response.status !== 200 && response.status !== 204) {
    throw new Error('Unexpected PATCH success status: ' + response.status);
  }
}

async function readWriteResponse(response) {
  if (!response.ok) {
    throw new Error('Write request failed: ' + response.status);
  }

  if (response.status === 204) {
    return null;
  }

  const data = await response.json();

  if (!isTask(data)) {
    throw new Error('Response did not contain a valid task record.');
  }

  return data;
}

Tip

Headers do not make a response valid

Accept: application/json is a preference, not proof of the returned format. Do not replace the status guard with a Content-Type guess. The documented status is checked first; then decoding and task-shape validation determine whether the JSON response can be used.

Keep examples separate from local evidence

Example

Illustrative API response

PATCH returns 200 OK with task JSON

{"id":7,"title":"Practice HTTP requests","completed":true}

The PATCH status guard accepts 200. The helper decodes and validates the task record. Write confirmed by the API.

PATCH returns 204 No Content

The PATCH status guard accepts 204. The helper returns null before calling response.json(). Write confirmed by the API.

PATCH returns 201 Created with task JSON

The status guard throws because 201 is not documented for PATCH. The JSON body is not used to confirm the write.

A rejected status, or 200 OK with HTML

A rejected status stops at response.ok. HTML reaches JSON decoding and fails. Neither outcome is confirmed.

Warning

Observed Vite response

Run npm run dev and inspect only what your own browser received in DevTools. In Network, select the request; use Headers to record its status and response content-type, and use Payload to inspect the outgoing request body.

Label those facts Observed Vite response. Your Vite project does not implement /api/tasks or /api/tasks/7, so its response is not one of the illustrative API responses and is not evidence that a task was stored. Do not assume a particular Vite status or fallback format.

Practice: complete the operation-specific helper

Complete and trace the PATCH response path

Complete and trace this response path for the documented PATCH operation. It must accept valid task JSON, return an empty result without decoding it when no content is permitted, reject an HTTP failure, and leave an HTML response unconfirmed.

javascript
const noContentStatus = ;

function isTask(value) {
  return value !== null &&
    typeof value === 'object' &&
    typeof value.id === 'number' &&
    typeof value.title === 'string' &&
    typeof value.completed === 'boolean';
}

async function readPatchResponse(response) {
  if () {
    throw new Error('Write request failed: ' + response.status);
  }

  if (response.status !== 200 && response.status !== noContentStatus) {
    throw new Error('Unexpected PATCH success status: ' + response.status);
  }

  return readWriteResponse(response);
}

async function readWriteResponse(response) {
  if (!response.ok) {
    throw new Error('Write request failed: ' + response.status);
  }

  if (response.status === noContentStatus) {
    return ;
  }

  const data = await response.();

  if (!isTask(data)) {
    throw new Error('Response did not contain a valid task record.');
  }

  return data;
}

Step 6 of 6

Apply the Complete Write Workflow

Construct and inspect a JSON task update locally, then use the documented response contract to distinguish confirmed writes from rejected or unconfirmed outcomes.

One workflow, two kinds of evidence

Use two stages

The final workflow has two separate checks. First, construct the documented partial update: PATCH /api/tasks/7 with the JSON request body { "completed": true }. Then inspect what the browser sent locally.

That inspection is request evidence, not storage evidence. Confirmation requires a response allowed by the PATCH contract:

  • 200 OK with a valid complete task record.
  • 204 No Content, represented by null without decoding a body.

A rejected status stops the path. An undocumented success status, such as 201 Created, also stops it. If a 200 response contains HTML or malformed JSON, the task record cannot be established, so the outcome remains unconfirmed.

PATCH decision flow

The caller owns the operation-specific status check. readWriteResponse(response) remains focused on rejection protection, the permitted 204 branch, JSON decoding, and task validation.

Separate request evidence from confirmation

A flow from a partial task update through a PATCH request and status gate to either a validated task, an empty successful response, or non-confirmation.

Inspect the outgoing request locally, then confirm only after the documented PATCH response path succeeds.

Tip

Keep the evidence separate

Workflow rule

  1. Match the documented operation to its endpoint and method.
  2. Serialize the partial update and inspect it in DevTools.
  3. Validate the PATCH status before calling readWriteResponse(response).
  4. Confirm only after the permitted body path succeeds.

Vite can help you inspect a request, but it does not implement this task API or demonstrate persistence.

Connect the request and response paths

Run the local PATCH example

Replace the contents of your own index.html and src/main.js with the complete examples below. Run npm run dev, open the URL reported by Vite, and use DevTools after clicking the button. The page intentionally has no status interface.

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>Inspect a task request</title>
  </head>
  <body>
    <main>
      <h1>Task request inspector</h1>
      <button id="send-request" type="button">Send task request</button>
    </main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
Expected output
Task request inspector
[Send task request]

When Vite serves this document, the browser displays the heading and button. Request facts and outcomes are examined in DevTools rather than on the page.

src/main.js

The PATCH-specific status check stays outside readWriteResponse(response). After assertPatchResponseStatus(response) accepts 200 or 204, the helper chooses the no-content or JSON-validation path. Any failure reaches the catch block without a confirmation.

src/main.js

javascript
const update = { completed: true };
const sendButton = document.querySelector('#send-request');

if (!sendButton) {
  throw new Error('Missing #send-request button.');
}

function isTask(value) {
  return value !== null &&
    typeof value === 'object' &&
    typeof value.id === 'number' &&
    typeof value.title === 'string' &&
    typeof value.completed === 'boolean';
}

function assertPatchResponseStatus(response) {
  if (!response.ok) {
    throw new Error('Write request failed: ' + response.status);
  }

  if (response.status !== 200 && response.status !== 204) {
    throw new Error('Unexpected PATCH success status: ' + response.status);
  }
}

async function readWriteResponse(response) {
  if (!response.ok) {
    throw new Error('Write request failed: ' + response.status);
  }

  if (response.status === 204) {
    return null;
  }

  const data = await response.json();

  if (!isTask(data)) {
    throw new Error('Response did not contain a valid task record.');
  }

  return data;
}

sendButton.addEventListener('click', async () => {
  try {
    const response = await fetch('/api/tasks/7', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(update)
    });

    console.log('Observed Vite response', {
      status: response.status,
      contentType: response.headers.get('content-type')
    });
    assertPatchResponseStatus(response);
    const data = await readWriteResponse(response);
    console.log('Write confirmed by the API.', data);
  } catch (error) {
    console.error('Write outcome is unconfirmed.', error);
  }
});
Expected output
Observed Vite response { status: <status observed locally>, contentType: <content type observed locally or null> }
Write outcome is unconfirmed. Error: <HTTP status, decoding, or validation error>

This is a template for an actual local observation, not a supplied API result. The status, content type, and error vary with your Vite response. Vite does not provide the illustrative task API, so do not treat any local result as confirmation.

Inspect without assuming a saved task

Inspect the local request

Open DevTools before clicking Send task request once. In Network, select the request whose URL ends in /api/tasks/7.

In Headers, record the Request Method, Content-Type, response status, and response content-type when present. In Payload, record the exact JSON request body.

Label the received status and response content type Observed Vite response. These facts verify the outgoing PATCH request. Your Vite project does not implement /api/tasks/7, so they do not show that task 7 was stored or changed.

Find the outgoing request facts

A DevTools-style request detail view with sections for Network, Headers, and Payload, highlighting a PATCH request and JSON update body.

Use Network, Headers, and Payload to verify what left the browser; record the received response separately.

Warning

Do not infer persistence

Observed Vite response is not an Illustrative API response.

Vite serves development resources rather than the writable task API used in this contract. Do not predict its status, its content type, or its fallback behavior. A rejected Vite response, HTML fallback, or any unreadable response must remain outside the success path.

Apply the request contract

Build the PATCH request

Complete the request configuration for the documented task-7 partial update.

javascript
const update = { completed: true };

const response = await fetch('/api/tasks/7', {
  method: '',
  headers: {
    '': '',
    'Accept': 'application/json'
  },
  body: 
});

Review the complete workflow

Complete write checklist

A five-stage visual checklist from documented operation through request inspection and response validation to conditional confirmation.

Documented operation → correct JSON request → inspected payload → checked response → contract-supported confirmation.

Workflow review

A complete workflow is evidence-based:

  • The partial update uses PATCH /api/tasks/7 and sends only { "completed": true } as JSON.
  • DevTools verifies the outgoing method, headers, and Payload, even if the local server does not support the operation.
  • assertPatchResponseStatus(response) accepts only 200 OK or 204 No Content for this PATCH contract.
  • readWriteResponse(response) returns a validated task for 200 or null for 204.
  • A rejected status, 201 Created, HTML, malformed JSON, or an invalid task shape must not produce a success confirmation.

Example

Interpret supplied response examples

Illustrative API response

  • 200 OK with {"id":7,"title":"Practice HTTP requests","completed":true} passes the status guard and task validation. Write confirmed by the API.
  • 204 No Content passes the status guard and returns null without JSON decoding. Write confirmed by the API.
  • 403 Forbidden is rejected by the HTTP gate.
  • 201 Created with task JSON is not a documented PATCH success, so it is unconfirmed.
  • 200 OK with an HTML document cannot produce a validated task, so it is unconfirmed.

These are contract examples, not console output from Vite.

Warning

Name the evidence accurately

Before the final verification, distinguish the labels carefully: an Illustrative API response demonstrates the documented contract, while an Observed Vite response is only what your own local server returned.

Final application

Verify the complete write workflow

Create one compact verification record for the complete workflow.

  1. Write the exact method, Content-Type, Accept, and body expression needed for the documented PATCH request.
  2. After running the local example, report the method and exact JSON body you observed in Network and Payload, plus the status and response content-type you recorded as the Observed Vite response.
  3. Classify each supplied response as confirmed, rejected, or unconfirmed: 200 OK with a valid task JSON record; 204 No Content; 403 Forbidden; 201 Created with task JSON; and 200 OK with an HTML document.
  4. Explain why Vite inspection does not demonstrate persistence and why rejected or unconfirmed outcomes cannot be reported as successful writes.

Write at least 420 characters (0/420).

Tutorial complete

Congratulations! You completed: Send JSON Data with HTTP Requests

You can now construct, inspect, and interpret JSON write requests without confusing a locally sent request with a confirmed API write.

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