Requests in depth
Build precise fetch requests with HTTP methods, headers, JSON bodies, Request and Response objects, credentials, and cache options.
- 01Choose the right methodExplain safe and idempotent methods and send REST-style reads, creates, replacements, patches, and deletes.
- 02Control headers and bodiesUse Headers, JSON.stringify, Content-Type, Accept, and Location without fighting browser rules.
- 03Inspect real web platform objectsConstruct Request and Response objects, clone one-use bodies, and reason about credentials and cache modes.
Beyond basic fetch
The previous lesson treated fetch as “ask for data, check ok, parse JSON.” Real applications also need to describe the request: which action should happen, which format you are sending, which format you want back, whether credentials may travel, and how the browser cache should behave.
This lesson stays static-site safe. Instead of calling an outside API, it builds a tiny in-page REST desk: fakeFetch(input, init) creates a real Request, routes it through an in-memory todo server, and returns real Response objects with status codes, JSON bodies, Location, and ETag headers.
Imagine an organized library desk. The verb you choose tells the librarian what kind of work you intend. Asking to see a book twice is harmless. Submitting the same “add this book” form twice may create two records.
- In real life: May I see this book?
- In JavaScript: GET reads a resource
- In real life: Please add this new book
- In JavaScript: POST creates or submits
- In real life: Replace this book with this edition
- In JavaScript: PUT replaces the whole resource
- In real life: Fix page 12
- In JavaScript: PATCH changes part of a resource
- In real life: Remove this book
- In JavaScript: DELETE removes a resource
Where the analogy stops: Libraries have people who may refuse unusual requests. HTTP methods describe intent; the server still decides what it actually supports.
Headers are notes around the message: “this letter is JSON,” “please reply in JSON,” or “here is my badge.” A Request is the completed order form. A Response is the reply form you inspect when it comes back.
- In real life: Notes on the envelope
- In JavaScript: Headers such as
Content-Type,Accept, andAuthorization - In real life: A filled-in order form
- In JavaScript: A
Requestobject you can clone before handing over - In real life: The reply form
- In JavaScript: A
Responseobject with status, headers, and body - In real life: Photocopy before filing
- In JavaScript:
clone()before reading a body twice
Where the analogy stops: Paper can be reread freely. Request and Response bodies are streams, so reading consumes them once.
GET, POST, PUT, PATCH, DELETE
INTERACTIVESafe means the method is intended only to read or ask, not change server state. Idempotent means making the same request once or many times has the same intended final effect. Those words help you predict retries, refreshes, and duplicate clicks.
| Method | Usual intent | Safe? | Idempotent? | Why it matters |
|---|---|---|---|---|
| GET | Read a resource | Yes | Yes | The same read can be repeated without intending a write. |
| HEAD | Read headers only | Yes | Yes | Useful for checking metadata without downloading the body. |
| OPTIONS | Ask what is allowed | Yes | Yes | Often used by browsers before some cross-origin requests. |
| POST | Create or submit | No | No, not in general | Submitting the same form twice may create two records. |
| PUT | Replace a resource | No | Yes | The same full replacement leaves the resource the same. |
| PATCH | Apply a partial change | No | No, not in general | Some patches are repeatable, but the method does not promise it. |
| DELETE | Remove a resource | No | Yes | After deletion, repeating the request does not remove another resource. |
GET, HEAD, and OPTIONS are safe and idempotent. PUT and DELETE are not safe because they change data, but they are idempotent. POST and PATCH are not idempotent in general: submitting the same order twice can make two orders, and a patch can be “increment by one.”
const response = await fakeFetch("/api/todos", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ title: "Pack a snack" }),}); console.log(response.status);console.log(response.headers.get("Location"));console.log(await response.json());- Ready
Choose a method and send it to the in-page REST desk.
[{"id":1,"title":"Read the fetch reply","completed":true,"version":1},{"id":2,"title":"Practice request methods","completed":false,"version":1}]Every click builds a real Request, routes it through a tiny in-memory todo API, and returns a real Response. Reset restores the server data.
The playground shows the server data changing. Try GET /api/todos, then POST /api/todos with a body, then PATCH /api/todos/2, then DELETE /api/todos/1. A GET or HEAD request cannot have a body; the real Request constructor throws TypeError for that combination.
- GET /api/todos
- HEAD /pricing
- PUT /api/todos/2 with the full replacement
- DELETE /api/todos/2
- POST /api/orders
- PATCH /api/todos/2 with increment-style change
Sort each action by its safety and repeat behavior.
Headers
LABHeaders is the web platform object for request and response headers. Names are case-insensitive: Content-Type, content-type, and CONTENT-TYPE name the same field. append adds a value. set replaces the existing value.
const headers = new Headers();headers.append("Accept", "application/json");headers.append("accept", "text/plain");headers.set("Content-Type", "application/json"); console.log(headers.get("ACCEPT"));console.log([...headers]);const headers = new Headers();headers.append("Accept", "application/json");headers.append("accept", "text/plain");headers.set("Content-Type", "application/json"); console.log(headers.get("ACCEPT"));console.log([...headers]);- Ready
Run the lab in your browser.
append adds values, set replaces, get ignores case, and browser-controlled headers are not something app code should rely on setting.
Some request headers are controlled by the browser. You should not rely on setting names such as Host or Cookie from app code; the browser and network stack decide what is actually sent. The lab reports what this browser lets the Headers object store, then reminds you that storing a header object is not the same as successfully sending that header on the network.
When the body is FormData, do not set Content-Type yourself. The browser adds a multipart/form-data value with a boundary that must match the encoded body. URLSearchParams is similar: the browser can label it as URL-encoded form data.
Sending JSON
STEP THROUGHSending JSON has three pieces: choose a method that may have a body, label the body with Content-Type: application/json, and turn the JavaScript value into text with JSON.stringify. The JSON lesson explains why JSON is text instead of a live object.
async function createTodo(title) { const headers = new Headers(); headers.set("Accept", "application/json"); headers.set("Content-Type", "application/json"); const body = JSON.stringify({ title }); const response = await fakeFetch("/api/todos", { method: "POST", headers, body, }); if (!response.ok) { const problem = await response.json(); throw new Error(response.status + " " + problem.message); } const location = response.headers.get("Location"); const todo = await response.json(); return { location, todo };}Choose a title scenario, then step through the same real fake-server result.
script
const headers = new Headers(); headers.set("Accept", "application/json"); headers.set("Content-Type", "application/json"); const body = JSON.stringify({ title }); const response = await fakeFetch("/api/todos", { method: "POST", headers, body, }); if (!response.ok) { const problem = await response.json(); throw new Error(response.status + " " + problem.message); } const location = response.headers.get("Location"); const todo = await response.json(); return { location, todo };}Switch the recording to “empty title” or “duplicate title.” The request still reaches the fake server, but the server replies with 422 or 409. The helper reads that JSON problem body and throws a useful Error for the caller.
201 Created includes a Location header for the new todo. 409 Conflict reports a duplicate title. 422 Unprocessable Content reports a body that was syntactically JSON but failed validation.
Request and Response objects
OBJECTSfetch accepts a URL plus options, or a ready-made Request. A Request is useful when you want to prepare method, headers, body, credentials, cache mode, signal, and other options now, then pass that package around later.
Requests and responses have one-use bodies. If you need to inspect the body for logging and also send or parse it, call clone() first. The same rule appeared in the previous lesson for response.json(); here you can see it on both sides of the exchange.
const request = new Request("/api/todos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Clone me" }),}); const copy = request.clone();console.log(await request.text());console.log(await copy.json()); const response = Response.json( { saved: true }, { status: 201, headers: { Location: "/api/todos/3" } },);console.log(response.status);console.log(response.headers.get("content-type"));- Ready
Run the object lab.
Request and Response bodies are one-use streams. Clone before reading twice. Response.json is a convenient static helper when the browser supports it.
const request = new Request("/api/todos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Clone me" }),}); const copy = request.clone();console.log(await request.text());console.log(await copy.json()); const response = Response.json( { saved: true }, { status: 201, headers: { Location: "/api/todos/3" } },);console.log(response.status);console.log(response.headers.get("content-type"));Response.json(data, init) is a static helper that creates a JSON Response and sets the content type for you. It is a modern baseline feature, so the lab feature-detects it. If you support an older browser, new Response(JSON.stringify(data), ...) is the fallback.
credentials and cache options
REAL CHECKcredentials controls whether browser credentials such as cookies, HTTP authentication, and client certificates may travel. The default is same-origin. Cross-origin credentials need include plus server CORS permission, which the next lesson explains.
| Value | Plain-language meaning |
|---|---|
| omit | Send no cookies, HTTP authentication, or client certificates. |
| same-origin | Default. Send credentials to the same origin, not cross-origin. |
| include | Send credentials even cross-origin, but the server must allow credentials with CORS. |
cache controls how this request talks to the browser’s HTTP cache. It does not magically make a server faster; it gives the browser instructions about cache lookup, revalidation, and storage.
| Value | Plain-language meaning |
|---|---|
| default | Use the browser's normal HTTP cache rules. |
| no-store | Do not read from or write to the HTTP cache for this request. |
| reload | Bypass the cache on the way out, then update it with the response. |
| no-cache | You may use a cached entry only after checking with the server. |
| force-cache | Prefer a cached entry if one exists, even if stale. |
| only-if-cached | Use the cache only; in browsers it must be same-origin mode. |
await fetch(location.href, { cache: "default" });await fetch(location.href, { cache: "no-store" }); await fetch("/api/profile", { credentials: "omit" });await fetch("/api/profile", { credentials: "same-origin" });await fetch("/api/profile", { credentials: "include" });- Ready
Run a same-origin HEAD check.
The real request uses this page only. If default and no-store look identical, that is still useful: many cache effects are observable only with server cache headers and repeat visits.
The real check uses fetch(location.href, { method: "HEAD" }) with cache: "default" and cache: "no-store". On many static pages the visible status and headers are identical. That is honest: cache modes are instructions, and you often need server cache headers or devtools to observe a difference.
Where you will use this
PRACTICALThese details show up in every app that saves data: todo lists, dashboards, profile editors, checkout flows, admin tools, and search pages. You choose a method, create headers, serialize a body, check the response status, and read a one-use body.
- Loading a dashboard:
GET /api/stats, perhaps withcache: "no-store"for live numbers. - Creating a todo:
POST /api/todoswith JSON and aLocationresponse header. - Saving a profile edit:
PUT /api/profilewhen the full profile form replaces server state. - Checking one checkbox:
PATCH /api/todos/2with only{ completed: true }. - Canceling a slow request: combine these options with AbortController.
Common misconceptions
WATCH OUT- “POST is always create.” POST usually submits to a collection or action, but the server defines the contract.
- “Idempotent means no response changes.” It means the intended server state is the same after repeats; status may differ, such as a repeated DELETE returning 404.
- “Header names are case-sensitive.” They are case-insensitive, so
headers.get("content-type")findsContent-Type. - “I should set Content-Type for every body.” Set it for JSON. Do not set it manually for
FormData. - “A Request body is just a string I can reread.” Bodies are streams. Clone before reading twice.
- “credentials: include bypasses CORS.” It does not. Cross-origin credentials require matching server CORS headers.
| Piece | Question it answers | Example |
|---|---|---|
| Method | What action do I intend? | PATCH means apply a partial change. |
| Content-Type | What format am I sending? | application/json for JSON text. |
| Accept | What format would I like back? | application/json asks for JSON. |
| credentials | May browser credentials travel? | same-origin is the default. |
| cache | How should the HTTP cache be used? | no-store avoids cache read/write. |
Practice exercises
5 EXERCISESYou want to create a new todo at /api/todos. Which method usually fits?
Use POST for the usual create/submit-to-a-collection request.
Predict the error name logged by this snippet.
try {
new Request("https://example.test/api", {
method: "GET",
body: "hello",
});
} catch (error) {
console.log(error.name);
}try {
new Request("https://example.test/api", {
method: "GET",
body: "hello",
});
} catch (error) {
console.log(error.name);
}The Request constructor throws TypeError because GET requests cannot have a body.
What two lines print?
const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("accept", "text/plain");
console.log(headers.get("ACCEPT"));
headers.set("Accept", "application/xml");
console.log(headers.get("accept"));const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("accept", "text/plain");
console.log(headers.get("ACCEPT"));
headers.set("Accept", "application/xml");
console.log(headers.get("accept"));The first read combines appended values. After set, the Accept header is replaced with application/xml.
What two lines print?
const request = new Request("https://example.test/api", {
method: "POST",
body: JSON.stringify({ title: "Clone" }),
});
const copy = request.clone();
console.log(await request.text());
console.log((await copy.json()).title);const request = new Request("https://example.test/api", {
method: "POST",
body: JSON.stringify({ title: "Clone" }),
});
const copy = request.clone();
console.log(await request.text());
console.log((await copy.json()).title);The original request prints the raw JSON text. The clone still has an unread body, so json() returns an object and .title prints Clone.
Should you manually set Content-Type when sending a FormData body?
No. Do not set Content-Type manually for FormData; the browser adds the multipart boundary that matches the encoded body.
Check your understanding
8 QUESTIONSQuestion 1 of 8Which method is both safe and idempotent?
Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictconst headers = new Headers(); headers.append("Accept", "application/json"); headers.append("accept", "text/plain"); console.log(headers.get("ACCEPT"));Choose an answer to see the explanation.
Question 3 of 8Which JSON request is correct?
Choose an answer to see the explanation.
Question 4 of 8What error name does this print?
Read the code, then predicttry { new Request("https://example.test/api", { method: "GET", body: "x" }); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 5 of 8Why clone a Request or Response?
Choose an answer to see the explanation.
Question 6 of 8Which credentials mode is the browser fetch default?
Choose an answer to see the explanation.
Question 7 of 8What is the safest rule for FormData bodies?
Choose an answer to see the explanation.
Question 8 of 8What does cache: no-store mean in plain language?
Choose an answer to see the explanation.
Key takeaways
- Methods express intent: GET reads, POST submits, PUT replaces, PATCH partially changes, DELETE removes.
- Safe methods are GET, HEAD, and OPTIONS. Idempotent methods are GET, HEAD, OPTIONS, PUT, and DELETE.
- Headers are case-insensitive. Use
setto replace andappendto add another value. - Send JSON with a body-capable method,
Content-Type: application/json, andJSON.stringify. - Request and Response bodies are one-use streams. Clone before reading twice.
credentialsdefaults tosame-origin; cache modes are instructions to the browser HTTP cache.
Final definition: A precise fetch request combines method, URL, headers, body, credentials, and cache instructions, then receives a Response whose status, headers, and one-use body describe the server’s answer.
Up next: CORS & cross-origin requests.