Handling POST Requests With JS Fetch API Guide

A complete guide to handling POST requests with the JavaScript Fetch API. Covers JSON payloads, form data, file uploads with FormData, setting headers, reading responses, error handling for POST requests, CSRF tokens, and building reusable POST helpers for common API patterns.

JavaScriptintermediate
11 min read

POST requests send data to a server to create or process resources. With the Fetch API, POST requests require explicit configuration of the method, headers, and body. This guide covers every POST pattern you will encounter in frontend development.

Basic JSON POST

javascriptjavascript
async function createUser(name, email) {
  const response = await fetch("/api/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ name, email }),
  });
 
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
 
  return response.json();
}
 
const newUser = await createUser("Alice", "alice@example.com");
console.log(newUser.id); // Server-assigned ID

The three required pieces: method: "POST", Content-Type header, and body with serialized data.

Content Types Comparison

Content-TypeBody FormatUse Case
application/jsonJSON.stringify(obj)API data exchange (most common)
application/x-www-form-urlencodedURLSearchParamsHTML form submission
multipart/form-dataFormDataFile uploads
text/plainRaw stringPlain text data

URL-Encoded Form Data

Traditional HTML form format:

javascriptjavascript
const formData = new URLSearchParams();
formData.append("username", "alice");
formData.append("password", "secret123");
formData.append("remember", "true");
 
const response = await fetch("/api/login", {
  method: "POST",
  body: formData,
  // Content-Type is set automatically to application/x-www-form-urlencoded
});

You can also pass an object to the constructor:

javascriptjavascript
const formData = new URLSearchParams({
  username: "alice",
  password: "secret123",
});

FormData for Multipart Uploads

javascriptjavascript
const form = new FormData();
form.append("title", "My Document");
form.append("category", "reports");
form.append("file", document.getElementById("file-input").files[0]);
 
const response = await fetch("/api/documents", {
  method: "POST",
  body: form,
  // Do NOT set Content-Type — the browser sets it with the boundary
});

Setting Content-Type manually for FormData breaks the request because the browser needs to generate the multipart boundary string. See uploading files via JS fetch API complete guide for advanced file upload patterns.

Reading POST Responses

Servers return different response formats. Handle each appropriately:

javascriptjavascript
// JSON response (most APIs)
const data = await response.json();
 
// Text response (plain text or HTML)
const text = await response.text();
 
// No-content response (204)
if (response.status === 204) {
  console.log("Success, no content");
  return null;
}
 
// Created response (201) with Location header
if (response.status === 201) {
  const location = response.headers.get("Location");
  const created = await response.json();
  console.log(`Created at: ${location}`);
}

Error Handling for POST Requests

POST errors often include validation details in the response body:

javascriptjavascript
async function submitForm(data) {
  const response = await fetch("/api/register", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
 
  if (!response.ok) {
    // Try to parse error response
    let errorBody;
    try {
      errorBody = await response.json();
    } catch {
      errorBody = { message: response.statusText };
    }
 
    // Handle specific status codes
    switch (response.status) {
      case 400:
        throw new ValidationError(errorBody.errors || []);
      case 401:
        throw new AuthError("Not authenticated");
      case 409:
        throw new ConflictError(errorBody.message);
      case 422:
        throw new ValidationError(errorBody.errors || []);
      default:
        throw new Error(`HTTP ${response.status}: ${errorBody.message}`);
    }
  }
 
  return response.json();
}
 
class ValidationError extends Error {
  constructor(errors) {
    super("Validation failed");
    this.errors = errors;
  }
}
 
class AuthError extends Error {}
class ConflictError extends Error {}

Including Authentication

javascriptjavascript
// Bearer token
const response = await fetch("/api/posts", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${accessToken}`,
  },
  body: JSON.stringify({ title: "New Post", content: "..." }),
});
 
// Cookie-based auth
const response2 = await fetch("/api/posts", {
  method: "POST",
  credentials: "include", // Send cookies with the request
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "New Post" }),
});

CSRF Protection

When using cookie-based auth, include a CSRF token:

javascriptjavascript
// Read CSRF token from a meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
 
const response = await fetch("/api/transfer", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": csrfToken,
  },
  body: JSON.stringify({ amount: 100, to: "bob@example.com" }),
});

Retry Pattern for POST Requests

Not all POST requests are safe to retry (non-idempotent). Only retry for network errors, not for HTTP errors:

javascriptjavascript
async function postWithRetry(url, data, maxRetries = 2) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
 
      // Don't retry HTTP errors (server received the request)
      return response;
    } catch (error) {
      // Only retry network errors
      if (attempt === maxRetries) throw error;
      await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
    }
  }
}

Reusable POST Helper

javascriptjavascript
async function post(url, data, options = {}) {
  const config = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...options.headers,
    },
    body: JSON.stringify(data),
    ...options,
  };
 
  // Remove Content-Type if using FormData
  if (data instanceof FormData) {
    delete config.headers["Content-Type"];
    config.body = data;
  }
 
  const response = await fetch(url, config);
 
  if (!response.ok) {
    const error = await response.json().catch(() => ({ message: response.statusText }));
    throw Object.assign(new Error(error.message), { status: response.status, body: error });
  }
 
  if (response.status === 204) return null;
  return response.json();
}
 
// Usage
const user = await post("/api/users", { name: "Alice" });
const file = await post("/api/upload", formData);
Rune AI

Rune AI

Key Insights

  • Three required pieces for JSON POST: method: "POST", Content-Type: application/json header, and JSON.stringify() body
  • Do not set Content-Type for FormData: Let the browser generate the multipart boundary automatically
  • Parse error response bodies: POST errors often include validation details that should be shown to the user
  • Only retry network errors for POST: HTTP errors (400, 422) mean the server received the request; retrying could create duplicates
  • Centralize POST logic in a helper: A reusable function handles headers, serialization, error parsing, and FormData detection
RunePowered by Rune AI

Frequently Asked Questions

Why do I need to stringify the body?

Fetch does not auto-serialize objects. The `body` option accepts strings, FormData, Blob, ArrayBuffer, or URLSearchParams. For JSON, you must call `JSON.stringify()` yourself.

What happens if I forget the Content-Type header?

The server may not parse the body correctly. Without `Content-Type: application/json`, most servers treat the body as plain text and fail to parse it as JSON.

Can I send a POST request with no body?

Yes. `fetch(url, { method: "POST" })` sends a POST with an empty body. This is valid for triggering server-side actions that need no input.

Should I use POST or PUT for updates?

POST creates new resources. PUT replaces an entire resource. PATCH updates part of a resource. Use the method that matches your API's semantics.

How do I send an array as POST data?

`JSON.stringify([item1, item2])` works fine. Set `Content-Type` to `application/json`. The server receives a JSON array.

Conclusion

POST requests with Fetch require three pieces: the method, the content type header, and the serialized body. Always check response.ok, handle validation errors from the response body, and build reusable helpers to avoid repeating boilerplate. For the full Fetch API reference, see how to use the JS fetch API complete tutorial. For file-specific POST patterns, see uploading files via JS fetch API complete guide.