Skip to main content

⚙️ Request Configuration Options

A URL tells fetch where to go. The optional second argument — the options object — tells it how to get there: which HTTP method to use, what headers and body to send, whether to include cookies, and how to handle caching and cancellation. This lesson is your field guide to every option that matters.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Pass an options object to fetch() and set the HTTP method
  • Send request headers and choose the right Content-Type for your body
  • Attach a body as JSON, FormData, or URLSearchParams
  • Explain mode, credentials, and cache, and when each matters
  • Use signal with AbortController to cancel or time out a request

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build a small request() wrapper that merges default options with per-call overrides.

In This Lesson

The Options Object

Every fetch you've written so far used a single argument. The full signature is fetch(url, options), where options is a plain object whose properties fine-tune the request:

fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ada', email: 'ada@example.com' }),
})
  .then((response) => response.json())
  .then((data) => console.log(data));
💡 An analogy: If the URL is the address on an envelope, the options object is everything else that governs delivery — first class or ground (method), the notes on the outside (headers), the contents inside (body), whether it's tracked and signed-for (credentials), and a way to recall it mid-transit (signal).
The fetch options object and its main properties A central options object branches into method, headers, body, mode, credentials, cache, and signal. options (2nd argument) method headers body credentials signal …plus mode, cache, redirect, referrerPolicy, integrity, keepalive
Figure 1 — The options object is where you shape a request. You'll use method, headers, and body constantly; the rest come up in specific situations.

HTTP Methods with method

The method property sets the HTTP verb. It defaults to 'GET'. Each verb signals a different intent to the server, and REST APIs lean on that convention heavily.

MethodIntentLibrary analogy
GETRead a resource (no changes)Reading a book on the shelf
POSTCreate a new resourceAdding a new book to the collection
PUTReplace a resource entirelySwapping in a whole new edition
PATCHUpdate part of a resourceCorrecting a few pages
DELETERemove a resourceTaking a book out of circulation
// GET (default) — just read
await fetch('https://api.example.com/users');

// POST — create
await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
});

// PATCH — partial update
await fetch('https://api.example.com/users/123', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'new@example.com' }),
});

// DELETE — remove
await fetch('https://api.example.com/users/123', { method: 'DELETE' });

💡 GET and HEAD carry no body

The spec forbids a request body on GET and HEAD. If you need to pass parameters on a GET, put them in the query string (see URLSearchParams below), not the body.

Request Headers with headers

Headers are metadata about your request — who you are, what format you're sending, what you'd like back. You can pass a plain object or a Headers instance.

// Plain object — simplest
await fetch('https://api.example.com/data', {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer eyJhbGciOiJIUzI1Ni...',
    'Accept': 'application/json',
  },
});

// Headers object — handy for building headers dynamically
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.set('Authorization', `Bearer ${token}`);
if (headers.has('Authorization')) {
  console.log(headers.get('Authorization'));
}
await fetch('https://api.example.com/data', { headers });
HeaderPurpose
Content-TypeThe format of the body you're sending (e.g. application/json)
AuthorizationCredentials — commonly a bearer token
AcceptThe format you'd like the response in
Cache-ControlCaching directives for the request

⚠️ Some headers are off-limits

The browser controls "forbidden" headers such as Host, Connection, Content-Length, and Cookie. Attempts to set them are silently ignored — the browser fills them in correctly for you.

The Request Body with body

The body carries the data you're sending, for methods like POST, PUT, and PATCH. Its type determines how you prepare it — and often which Content-Type to set (or not set).

flowchart TD A[Request body] --> B["JSON.stringify(obj)"] A --> C["new FormData(form)"] A --> D["new URLSearchParams(...)"] A --> E["a Blob or File"] B --> B1["Content-Type: application/json"] C --> C1["multipart/form-data — set by browser"] D --> D1["application/x-www-form-urlencoded"]

JSON — the everyday case

await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'John Doe', age: 30 }),
});

FormData — files and multipart

const formData = new FormData();
formData.append('username', 'johndoe');
formData.append('avatar', fileInput.files[0]);

await fetch('https://api.example.com/profile', {
  method: 'POST',
  body: formData, // note: do NOT set Content-Type yourself
});

⚠️ Don't set Content-Type for FormData

With FormData, the browser must generate a multipart/form-data header that includes a unique boundary string. If you hard-code Content-Type yourself, you'll omit that boundary and the server won't be able to parse the upload. Let the browser handle it.

URLSearchParams — form-encoded data

const params = new URLSearchParams();
params.append('search', 'query term');
params.append('sort', 'ascending');

await fetch('https://api.example.com/search', {
  method: 'POST',
  body: params, // sets application/x-www-form-urlencoded automatically
});

Mode & Credentials

mode — cross-origin behavior

The mode option governs how the request deals with CORS (Cross-Origin Resource Sharing), the browser rule that a page may only read responses from other origins when the server opts in.

  • 'cors' (default) — a normal cross-origin request; the server must send the right Access-Control-* headers for you to read the response.
  • 'same-origin' — fail outright if the URL is on a different origin.
  • 'no-cors' — allowed for a narrow set of "simple" requests, but you get an opaque response whose body you cannot read.

⚠️ no-cors does not bypass CORS

A common myth is that mode: 'no-cors' "turns off" CORS restrictions. It does the opposite — it produces a more restricted, opaque response you can't inspect. The real fix for a CORS error is to configure the server, or route through your own backend proxy.

credentials — cookies and auth

This controls whether cookies and HTTP-auth headers ride along with the request:

  • 'same-origin' (default) — send credentials only to the same origin.
  • 'include' — always send them, even cross-origin.
  • 'omit' — never send them.
// Send the session cookie to a cross-origin API
await fetch('https://api.example.com/profile', {
  credentials: 'include',
});

💡 The server must agree

For credentials: 'include' to work cross-origin, the server must respond with Access-Control-Allow-Credentials: true and an explicit origin (not the * wildcard). Otherwise the browser blocks the response.

Cache, Redirect & Signal

cache — talking to the HTTP cache

The cache option decides how the request interacts with the browser's HTTP cache:

ValueBehavior
'default'Normal caching rules apply
'no-store'Never read from or write to the cache — good for real-time data
'no-cache'Revalidate with the server before using a cached copy
'reload'Bypass the cache for the request, but store the fresh response
'force-cache'Use a cached copy even if stale

redirect

Controls redirect handling: 'follow' (default, follow automatically), 'error' (reject on a redirect), or 'manual' (hand you the redirect to deal with yourself).

signal — cancellation and timeouts

Perhaps the most useful advanced option. An AbortController produces a signal you pass into fetch; calling controller.abort() cancels the in-flight request. This powers timeouts and "cancel the old search when a new keystroke arrives" patterns.

// A fetch that times out after 5 seconds
async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), ms);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${ms}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

💡 Modern shortcut

Recent browsers support AbortSignal.timeout(5000), which you can pass directly as signal to get a timeout without wiring up your own setTimeout. The manual version above still works everywhere and shows what's happening under the hood.

Hands-on Exercise

🏋️ Build a configurable request() wrapper

Objective: Combine what you've learned into one function that holds sensible defaults and lets each call override them — the seed of a real API client.

Instructions:

  1. Write request(url, options = {}) that merges a set of default headers with any options.headers the caller passes.
  2. Default the Content-Type to application/json and credentials to same-origin.
  3. Add a 8-second timeout using AbortController and signal.
  4. Throw a clear error when !response.ok, and otherwise return parsed JSON.
  5. Test it with a GET to https://jsonplaceholder.typicode.com/posts/1 and a POST that creates a post.
💡 Hint

Merge order matters: spread the defaults first, then the caller's options, and merge headers as a nested object so a caller can add a header without wiping your defaults — headers: { ...defaults.headers, ...options.headers }.

✅ Sample solution
async function request(url, options = {}) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 8000);

  const defaults = {
    headers: { 'Content-Type': 'application/json' },
    credentials: 'same-origin',
  };

  try {
    const response = await fetch(url, {
      ...defaults,
      ...options,
      headers: { ...defaults.headers, ...options.headers },
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') throw new Error('Request timed out');
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

// GET
const post = await request('https://jsonplaceholder.typicode.com/posts/1');

// POST
const created = await request('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  body: JSON.stringify({ title: 'Hello', body: 'World', userId: 1 }),
});

🎯 Quick Quiz

Question 1: You're uploading a file with FormData. What should you do about the Content-Type header?

Question 2: What does mode: 'no-cors' actually do?

Question 3: Which option lets you cancel an in-flight fetch (for a timeout or a superseded search)?

Best Practices

✅ Do

  • Be explicit about method and Content-Type — it makes intent obvious to readers.
  • Centralize defaults in a wrapper and merge per-call overrides carefully (spread headers as a nested object).
  • Add a timeout via signal so requests can't hang forever.
  • Reach for credentials: 'include' only when you truly need cross-origin cookies.

⚠️ Don't

  • Don't set Content-Type when the body is FormData.
  • Don't expect no-cors to solve a CORS error — fix the server or proxy instead.
  • Don't attach a body to a GET or HEAD request; it's not allowed.

Summary & Quiz

🎉 Key Takeaways

  • The options object is fetch's control panel: method, headers, body, and more.
  • Match the body type to the job — JSON for structured data, FormData for uploads, URLSearchParams for form encoding.
  • mode and credentials govern cross-origin behavior; no-cors restricts rather than frees.
  • signal + AbortController give you cancellation and timeouts.
  • Wrapping defaults in a small helper keeps your requests consistent and DRY.

📚 Further Reading

🚀 What's Next?

You now control exactly how a request goes out. Next we turn to what comes back — processing the Response object in depth: status properties, headers, body methods, cloning, and streaming.

🎉 Well done!

With the options object in hand, you can send any request an API asks for. Let's decode the responses next.