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.
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
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 IDThe three required pieces: method: "POST", Content-Type header, and body with serialized data.
Content Types Comparison
| Content-Type | Body Format | Use Case |
|---|---|---|
application/json | JSON.stringify(obj) | API data exchange (most common) |
application/x-www-form-urlencoded | URLSearchParams | HTML form submission |
multipart/form-data | FormData | File uploads |
text/plain | Raw string | Plain text data |
URL-Encoded Form Data
Traditional HTML form format:
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:
const formData = new URLSearchParams({
username: "alice",
password: "secret123",
});FormData for Multipart Uploads
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:
// 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:
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
// 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:
// 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:
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
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
Key Insights
- Three required pieces for JSON POST:
method: "POST",Content-Type: application/jsonheader, andJSON.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
Frequently Asked Questions
Why do I need to stringify the body?
What happens if I forget the Content-Type header?
Can I send a POST request with no body?
Should I use POST or PUT for updates?
How do I send an array as POST data?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.