Any time you start wiring up API calls, headers become part of the game right away. Things like auth tokens, content types, or custom metadata all need a place to live, and Axios gives you a clean way to manage them. That's where Axios set headers patterns come in: simple tools that help you keep requests organized without repeating yourself.
This guide walks through the approaches devs actually use in 2025: per-request headers, global defaults, interceptors, dynamic values, and the troubleshooting steps that save you from chasing weird bugs at 2 a.m.
Let's roll!

Quick answer (TL;DR)
To set headers in Axios, pass a headers object in the request config or define global defaults. Here's the fastest way to see it in action.
import axios from 'axios';
// Per-request headers
await axios.get('https://api.example.com/user', {
headers: {
'X-API-Key': '123',
'Accept': 'application/json'
}
});
// Global headers (applied to all requests)
axios.defaults.headers.common['X-API-Key'] = '123';
// Axios instance with its own headers
const api = axios.create({
baseURL: 'https://api.example.com',
headers: {
Authorization: 'Bearer token-123'
}
});
await api.post('/update', { ok: true });
Convert a cURL command into Axios easily with our online tool!
Installing Axios and basic setup
Before we start talking about Axios set headers, let's make sure the library is actually in your project. The setup is simple whether you're in Node.js or the browser. Note that we'll be using Axios v1 and Node 20+.
If you ever need to flip cURL commands into JS code, check out our online converter that can convert cURL commands to Axios.
Installing Axios via npm or CDN
Use whichever package manager you roll with:
npm install axios
# or
yarn add axios
# or
pnpm add axios
If you're in a browser and don't want a build step:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
Importing Axios in Node.js and browser environments
ES modules (Node.js or modern bundlers):
import axios from 'axios';
CommonJS (older Node.js setups):
const axios = require('axios');
Browser (after loading via CDN):
// Axios is available as a global value
axios.get('/api/user');
If you're dealing with JSON APIs and want a refresher on cURL basics, check out our guide: How to get JSON with cURL?
Setting headers for individual requests
Most of the time you only need headers on one call. Axios makes this easy: just pass a headers object in the config.
If you want a quick refresher on sending headers with cURL, check our tutorial: How to send HTTP headers using cURL?
axios.get() with headers object
// We'll prefer ESM for the examples
// in this tutorial
import axios from 'axios';
// simple GET request with custom headers
await axios.get('https://api.example.com/user', {
headers: {
'X-API-Key': '123', // custom API key for auth
'Accept': 'application/json' // ask the server for JSON
}
});
You pass a headers object in the request config. Axios sends these headers only for this call, so you can keep things flexible without touching global defaults.
axios.post() with headers and data
import axios from 'axios';
await axios.post(
'https://api.example.com/users',
{ name: 'Lee', role: 'admin' }, // request body
{
headers: {
'Content-Type': 'application/json', // tells server how to parse the body
'X-Client': 'demo' // custom header for tracking/debug
}
}
);
You send both data and headers in a single call. Axios includes the body as JSON and attaches the custom headers only for this request, keeping everything self-contained.
Axios set headers example for PUT and DELETE
// PUT: update user data
await axios.put(
'https://api.example.com/users/42',
{ role: 'editor' }, // updated fields
{
headers: {
Authorization: 'Bearer token-123' // auth token for protected routes
}
}
);
// DELETE: remove a user
await axios.delete('https://api.example.com/users/42', {
headers: {
Authorization: 'Bearer token-123' // same token required for deletion
}
});
Both requests modify server data, so they need authorization. You attach the token in each call. PUT sends a body plus headers, DELETE sends only headers. Axios handles both with the same config pattern.
Setting global headers in Axios
Sometimes you don't want to pass headers on every single call. Maybe you're always talking to the same API, maybe you're hitting a service like ScrapingBee and every request needs an API key. Global headers keep your code clean. They also make it harder to forget something important when you build bigger flows around axios set headers.
If you need a robust scraping solution that plays nicely with Axios (and lets you avoid dealing with headless browsers or proxy rotation yourself), ScrapingBee is a solid option. Check ScrapingBee pricing now!
axios.defaults.headers.common usage
This sets headers for all HTTP methods:
import axios from 'axios';
// applies to GET, POST, PUT, DELETE, everything
axios.defaults.headers.common['X-API-Key'] = '123';
// simple GET now carries the header automatically
await axios.get('https://api.example.com/data');
You attach a header once, and Axios includes it in every request unless you override it later.
Axios set headers globally for specific methods
Sometimes you only want defaults for POST or PUT calls. Axios gives you method-level buckets:
axios.defaults.headers.post['Content-Type'] = 'application/json';
await axios.post('https://api.example.com/save', { ok: true });
// this POST gets the content type automatically
Only POST requests inherit this header. Other methods stay untouched.
axios.create() for isolated header configs
If you need different configurations depending on the target API, create separate Axios instances. This is common when you mix internal APIs with external tools like ScrapingBee.
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
headers: {
Authorization: 'Bearer token-123'
}
});
const scrapingBeeClient = axios.create({
baseURL: 'https://app.scrapingbee.com/api/v1'
// provide any other config params
// specifically for this ScrapingBee instance...
});
// each instance manages its own headers
await apiClient.get('/users');
await scrapingBeeClient.get('/', {
params: {
api_key: 'YOUR-API-KEY',
url: 'https://example.com'
}
});
Each client keeps its own config. No shared state, no accidental header leaks across services.
Using interceptors to modify headers
Interceptors let you hook into every request or response and adjust headers on the fly. They're perfect when you need to attach tokens, refresh them, or inject something based on the current user state. This keeps your axios set headers logic clean and automatic.
axios.interceptors.request.use() for authorization
Use a request interceptor to attach auth tokens without repeating yourself:
import axios from 'axios';
// Browser-only interceptor:
// Reads the token from localStorage before every request
axios.interceptors.request.use((config) => {
// localStorage exists only in browsers
const token = localStorage.getItem('auth_token');
// If token is present, attach it to the Authorization header
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Always return the config so the request continues
return config;
});
Before the request leaves your app, Axios plugs in the token. No need to set it manually for every request.
In Node you don't have local storage, therefore the token should be stored somewhere else (memory, a module, a DB, etc). Here's an example:
import axios from 'axios';
// Example: token stored in memory (module-level variable)
let authToken = null;
// Some auth flow sets the token:
function setToken(t) {
authToken = t;
}
axios.interceptors.request.use((config) => {
if (authToken) {
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
});
// Example usage:
// setToken("abc123");
axios.interceptors.response.use() for token refresh
If an API sends back a "token expired" signal, you can catch that and refresh the token:
import axios from "axios";
// Main API client — your app makes all normal requests through this
const api = axios.create();
// Separate client ONLY for token refresh requests
// Important: this client has NO interceptors, so a failed refresh
// request cannot recursively trigger another refresh.
const authClient = axios.create();
/**
* Calls the refresh endpoint to get a new access token.
* Use the authClient to avoid recursion.
*/
async function refreshAuthToken() {
const current = localStorage.getItem("auth_token");
const res = await authClient.post("/auth/refresh", {
token: current,
});
// Assume API returns: { token: "new-token" }
return res.data.token;
}
// Attach a response interceptor to the main client
api.interceptors.response.use(
// Pass through any successful response unchanged
(response) => response,
// Handle failed responses
async (error) => {
const originalConfig = error.config;
// If there's no config or no response → nothing to do
if (!originalConfig || !error.response) {
return Promise.reject(error);
}
// If we got a 401 (token expired) and this request hasn't been retried yet:
if (error.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true; // mark so we never retry twice
// Get a new access token
const newToken = await refreshAuthToken();
// Store it locally
localStorage.setItem("auth_token", newToken);
// Ensure headers object exists
originalConfig.headers = originalConfig.headers || {};
// Inject the new token for the retry
originalConfig.headers.Authorization = `Bearer ${newToken}`;
// Retry the original request using the main api client
return api(originalConfig);
}
// For all other errors: reject as usual
return Promise.reject(error);
}
);
What this logic does:
- Intercepts failed responses from the main API client, looking specifically for
401errors caused by expired access tokens. - Prevents infinite loops by marking each request with a private
_retryflag, ensuring the retry happens only once. - Calls the refresh endpoint using a separate Axios instance (
authClient) so the refresh request itself is never intercepted or retried — this removes all recursion risk. - Stores the newly issued token and injects it into the original request's
Authorizationheader before retrying. - Retries the original API request transparently with the fresh token, while any other errors pass through unchanged.
Here's the Node.js version without local storage:
import axios from 'axios';
// Token storage (Node.js side)
// In real apps you might use Redis/DB/secret manager instead
let authToken = null;
function setAuthToken(token) {
authToken = token;
}
// Main axios instance for your app requests
const api = axios.create({
baseURL: 'https://api.example.com',
});
// Separate axios instance JUST for auth/refresh
const authClient = axios.create({
baseURL: 'https://api.example.com',
});
// Example refresh function (replace URL/body with your real API)
async function refreshAuthToken() {
if (!authToken) {
throw new Error('No auth token set, cannot refresh');
}
const res = await authClient.post('/refresh', {
token: authToken,
});
// Assume the new access token comes back as res.data.token
return res.data.token;
}
// Response interceptor: handles expired tokens
api.interceptors.response.use(
// Pass successful responses straight through
(response) => response,
// Handle failed responses
async (error) => {
const originalConfig = error.config;
// If we don't have a config, or no response, just bail out
if (!originalConfig || !error.response) {
return Promise.reject(error);
}
const status = error.response.status;
// Only handle 401 once per request
if (status === 401 && !originalConfig._retry) {
originalConfig._retry = true; // mark as retried to avoid infinite loop
try {
// 1) Get a fresh token
const newToken = await refreshAuthToken();
// 2) Save it in our Node token store
setAuthToken(newToken);
// 3) Ensure headers exist and update Authorization
originalConfig.headers = originalConfig.headers || {};
originalConfig.headers.Authorization = `Bearer ${newToken}`;
// 4) Retry the original request using the main axios instance
return api(originalConfig);
} catch (refreshError) {
// Refresh failed → bubble that up (user may need to re-login)
return Promise.reject(refreshError);
}
}
// Not a handled case → just reject the original error
return Promise.reject(error);
}
);
Notes for production:
localStorageis browser-only — in Node.js use something like:- an in-memory variable (simple apps) or a shared module-level store
- a database (Redis, Postgres, etc.)
- a dedicated auth/session service
- Don't refresh tokens for the refresh endpoint itself.
- Add global retry limits if needed.
Role-based header injection using localStorage
You can also set headers based on the user's role (admin, editor, viewer, etc.). Useful when the backend changes behavior depending on who's calling.
// This pattern is for browser apps (using localStorage).
// In Node.js, store the role in memory, a DB, or your auth layer instead.
axios.interceptors.request.use((config) => {
const role = localStorage.getItem('user_role'); // "admin", "editor", etc.
if (role) {
config.headers['X-User-Role'] = role;
}
return config;
});
Every request automatically carries the role header, so your backend can decide what the user is allowed to do.
Advanced header use cases and troubleshooting
Once you start building bigger flows, headers can get tricky. Environment-based logic, conditional rules, CORS limits, HTTPS warnings — all the fun stuff. These patterns come up a lot when integrating custom APIs or scraping tools like ScrapingBee, where you often pass keys, proxies, and extra metadata.
Understanding how Axios merges headers (global vs instance vs request)
When headers come from multiple places, Axios doesn't just mash them together randomly. There's a clear order, and on top of that, interceptors get a chance to modify the final config.
Here's the basic idea:
- Library defaults (built-in Axios defaults)
- Global defaults (
axios.defaults.headers) - Instance defaults (
api.defaults.headers/axios.create({ headers: ... })) - Per-request config (headers passed to
axios.get(url, { headers }), etc.) - Request interceptors (can mutate
config.headersright before the request goes out)
Config precedence is: library defaults → axios.defaults → instance defaults → per-request config. After that, request interceptors run and can override anything. Whatever writes last wins.
Example: who wins when everything sets the same header?
import axios from 'axios';
// 1. Global default
axios.defaults.headers.common['X-Source'] = 'global';
// 2. Instance default
const api = axios.create({
headers: { 'X-Source': 'instance' }
});
// 3. Request interceptor (runs after config is merged)
api.interceptors.request.use((config) => {
config.headers['X-Source'] = 'interceptor';
return config;
});
// 4. Per-request override
await api.get('/data', {
headers: { 'X-Source': 'request' }
});
What the server actually receives:
X-Source: interceptor
Why?
- Global defaults set
'global'. - The instance sets
'instance'and overrides the global value for this client. - The per-request config sets
'request'and overrides both defaults. - The interceptor runs last, sees the merged config, and sets
'interceptor', so that's what the server receives.
If you want per-request headers to "win" even in the presence of interceptors, you need to respect existing values in the interceptor:
api.interceptors.request.use((config) => {
// Only set if nothing else set it before
if (!config.headers['X-Source']) {
config.headers['X-Source'] = 'interceptor';
}
return config;
});
Now the behavior becomes:
- If the request doesn't specify
X-Source, the interceptor fills it in. - If the request does specify
X-Source, the interceptor leaves it alone and your per-request header wins.
Dynamic headers based on environment variables
Use process.env to avoid hardcoding sensitive values:
import axios from 'axios';
axios.defaults.baseURL = process.env.API_URL;
axios.defaults.headers.common['X-API-Key'] = process.env.API_KEY;
Your build pipeline injects environment variables, and Axios takes them directly. No secrets in source control.
Conditional headers using request URL or method
You can adjust headers depending on where the request is going or what method it uses:
axios.interceptors.request.use((config) => {
// If the request URL starts with "/admin",
// attach a custom header so the backend knows
// this is an admin-level operation.
if (config.url.startsWith('/admin')) {
config.headers['X-Admin-Access'] = 'true';
}
// If the request method is POST,
// add a header identifying where the request originated.
// Useful for analytics, debugging, or conditional logic on the server.
if (config.method === 'post') {
config.headers['X-POST-Source'] = 'dashboard';
}
// Always return the config so Axios can continue the request
return config;
});
You inspect the request before it's sent and attach headers only when needed.
Handling CORS and insecure HTTPS requests
When working with Axios, it's important to remember that CORS and HTTPS rules come from the platform, not from Axios itself. Browsers enforce strict security policies, while Node.js gives you far more freedom.
Browsers: strict rules you cannot bypass
When Axios runs in the browser, it is limited by the browser security policies:
- CORS is enforced by the browser. Requests to another origin are blocked unless the server explicitly allows them with headers like
Access-Control-Allow-OriginandAccess-Control-Allow-Headers. - You can't turn CORS off in Axios or fetch. There's no client-side flag to bypass it — only the server can permit the request.
- Some headers are restricted. Browsers block custom or sensitive headers unless the server whitelists them.
- Cookies / credentials require both sides to cooperate. The client must set
withCredentials: true, and the server must allow credentials with a non-wildcard origin. - Browsers reject insecure HTTPS. Self-signed or invalid certificates cause the browser to block the request before Axios runs.
Because of these rules, browser-based scraping or cross-origin access is often problematic. Services like ScrapingBee avoid these issues by performing all requests server-side.
Node.js: much more freedom
When Axios runs in Node.js, there is:
- No CORS enforcement as Node doesn't have the concept of web origins.
- No blocked headers — you can send anything you want.
- Full control over HTTPS behavior, including insecure certificates.
For example, connecting to a server with a self-signed certificate:
import https from 'https';
import axios from 'axios';
const agent = new https.Agent({
// This disables TLS security and should only
// be used in controlled development environments:
rejectUnauthorized: false // ⚠️ allows self-signed / invalid HTTPS certs
});
await axios.get('https://self-signed.test', { httpsAgent: agent });
This works in Node.js because Node lets you override TLS validation. Browsers don't allow this and insecure HTTPS is blocked at the networking layer before Axios sees anything.
Common header casing issues in Axios
Axios normalizes header names, but some buggy backends can still be picky. If you're dealing with strict or legacy servers, stick to standard casing for custom headers to avoid weird issues.
Bad:
headers: {
'x-api-key': '123'
}
Better:
headers: {
'X-API-Key': '123'
}
Axios will send them, but the backend might not recognize the lowercase variant. Stick to standard casing to avoid chasing weird bugs.
Missing cookies or session data when using Axios
Another super common issue: your API works in tools like cURL or Postman, but Axios "forgets" your cookies. This usually happens because browsers block cross-site cookies unless you explicitly allow credentials.
If your API uses sessions, CSRF tokens, or login cookies, you must enable the withCredentials flag.
Axios instance or global setting:
axios.defaults.withCredentials = true;
Per-request setting:
await axios.get('https://api.example.com/user', {
withCredentials: true
});
For cross-site requests made from the browser, cookies are only sent if withCredentials is enabled and the server's CORS settings allow it. Same-origin requests include cookies by default.
Also make sure your server is configured correctly:
Access-Control-Allow-Credentials: trueAccess-Control-Allow-Originmust not be*SameSite/Securecookie attributes must match your setup
File upload issues caused by manual Content-Type headers
One of the most common Axios pitfalls is manually setting Content-Type: multipart/form-data when you're already sending a real FormData object.
When you use a genuine FormData instance, Axios (or the browser / Node runtime) automatically generates the correct Content-Type header including the required boundary.
When Axios sends FormData, the actual header looks like:
multipart/form-data; boundary=----AxiosFormBoundary9aZk...
If you set Content-Type manually, Axios can't add that boundary, and the server receives a malformed payload. This leads to errors like "no file received," "invalid multipart payload," or empty form fields.
Bad (don't do this):
const form = new FormData();
form.append('file', file);
await axios.post('/upload', form, {
headers: {
'Content-Type': 'multipart/form-data' // ❌ removes the boundary → upload breaks
}
});
This rule applies only when sending a real
FormDataobject. If you're letting Axios buildFormDatafrom a plain JS object (per the official docs), then settingContent-Type: multipart/form-datais expected; Axios will serialize the body and attach the boundary for you.
Good (Node.js, Axios handles everything):
import FormData from 'form-data';
import fs from 'fs';
import axios from 'axios';
const form = new FormData();
form.append('file', fs.createReadStream('./file.txt'));
await axios.post('https://api.example.com/upload', form, {
headers: form.getHeaders() // ✔ includes multipart/form-data + boundary
});
form.getHeaders() comes from Node.js FormData libraries such as form-data or formdata-node.
Browser example:
const form = new FormData();
form.append('file', fileInput.files[0]);
await axios.post('/upload', form); // ✔ browser sets Content-Type + boundary
In the browser, never set Content-Type yourself when sending FormData as the browser attaches the correct boundary automatically.
Ready to power your API calls?
If you're done wrestling with headers, tokens, CORS, and flaky sites, you don't have to brute-force everything yourself. A scraping API like ScrapingBee handles proxies, browsers, headers, and blocks so you can focus on actual product work instead of infrastructure.
Conclusion
Setting headers in Axios isn't complicated once you break it down. You can set them per request, apply global defaults, or hook into interceptors to automate the whole flow. Whether you're dealing with auth tokens, roles, environment-based values, or tricky CORS setups, you've now got the patterns that actually show up in real projects.
Use these tools to keep your code clean, predictable, and easy to extend. And when you need something stronger than hand-rolled requests (scraping, proxy rotation, JavaScript rendering) pairing Axios with a service built for that job will save you a ton of time, so ScrapingBee will be a great pick.
Before you go, check out these related reads:
Frequently asked questions (FAQs)
How do I set Axios headers only once?
Use axios.defaults.headers or create an Axios instance with preconfigured headers. This way you define them a single time, and every request automatically includes them unless you explicitly override them in a request config.
Why are my Axios headers not applied?
Common reasons include CORS blocking your custom headers, browser restrictions, typos in header casing, or interceptors modifying the config unexpectedly. Double-check your header names, confirm the server allows them, and inspect any interceptors that might overwrite values.
Can I override global Axios headers?
Yes. Any header you pass directly in a request config takes priority over global defaults. This lets you keep global settings for most cases while customizing individual calls when you need something different, such as a different content type or temporary token.
How do I add Authorization headers dynamically?
Use a request interceptor that reads the current token from storage or memory and injects it into the header before each request. This avoids manually updating every call and ensures the latest token is always included in outbound requests.

Kevin worked in the web scraping industry for 10 years before co-founding ScrapingBee. He is also the author of the Java Web Scraping Handbook.

