Content-Type and MIME Types: Headers, Uploads, and API Responses
Get Content-Type right for APIs, file uploads, and static assets — charset parameters, multipart boundaries, sniffing risks, and a practical MIME lookup workflow.
A practical guide to consuming REST APIs in frontend applications — HTTP methods, status codes, fetch patterns, authentication, error handling, and debugging techniques.
By codefunc
Frontend applications talk to backends through APIs. REST (Representational State Transfer) is the dominant pattern — resources identified by URLs, actions expressed through HTTP methods, data exchanged as JSON. Understanding REST is not optional for frontend developers; it is how your UI gets data, how user actions persist, and where half your bugs will come from.
Bottom line: REST APIs use HTTP methods (GET, POST, PUT, DELETE) to perform operations on resources (URLs). Responses include status codes that tell you what happened. Handle errors gracefully — networks fail, servers error, tokens expire. Parse JSON carefully. Log enough to debug, but never log secrets.
A REST API exposes resources at URLs:
GET /users → List all users
GET /users/123 → Get user 123
POST /users → Create a user
PUT /users/123 → Replace user 123
PATCH /users/123 → Update user 123 partially
DELETE /users/123 → Delete user 123
URLs are nouns (resources), HTTP methods are verbs (actions).
| Method | Purpose | Idempotent | Has body |
|---|---|---|---|
| GET | Read resource | Yes | No |
| POST | Create resource | No | Yes |
| PUT | Replace resource | Yes | Yes |
| PATCH | Partial update | Yes | Yes |
| DELETE | Remove resource | Yes | Rarely |
Idempotent means calling it multiple times has the same effect as calling it once.
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"name": "Ada Lovelace",
"email": "ada@example.com"
}
Components:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/456
{
"id": "456",
"name": "Ada Lovelace",
"email": "ada@example.com",
"createdAt": "2026-07-07T10:00:00Z"
}
Components:
Status codes tell you what happened. Learn the categories:
| Range | Category | Examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved, 304 Not Modified |
| 4xx | Client error | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 5xx | Server error | 500 Internal Error, 502 Bad Gateway, 503 Unavailable |
| Code | Meaning | Frontend action |
|---|---|---|
| 200 | Success | Use the data |
| 201 | Created | Resource created, maybe redirect |
| 204 | No Content | Success, no body (delete) |
| 400 | Bad Request | Show validation errors |
| 401 | Unauthorized | Redirect to login |
| 403 | Forbidden | Show access denied |
| 404 | Not Found | Show not found page |
| 422 | Unprocessable | Show validation errors |
| 429 | Too Many Requests | Back off, retry later |
| 500 | Server Error | Show generic error, log |
| 503 | Unavailable | Show maintenance message |
Use the HTTP Status Code Lookup when debugging unfamiliar status codes.
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json();
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Ada Lovelace',
email: 'ada@example.com',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const user = await response.json();
fetch only rejects on network errors, not HTTP errors:
// This doesn't throw on 404 or 500!
const response = await fetch('/api/users/999');
// You must check response.ok or response.status
if (!response.ok) {
// Handle error
}
const params = new URLSearchParams({
page: '1',
limit: '20',
sort: 'createdAt',
order: 'desc',
});
const response = await fetch(`/api/users?${params}`);
// PUT: Replace entire resource
await fetch('/api/users/123', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Ada Lovelace',
email: 'ada@example.com',
role: 'admin',
}),
});
// PATCH: Update specific fields
await fetch('/api/users/123', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
role: 'admin',
}),
});
const response = await fetch('/api/users/123', {
method: 'DELETE',
});
if (response.status === 204) {
// Successfully deleted, no body
}
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
For debugging JWT issues, decode tokens with the JWT Decoder to check claims like exp (expiration) and aud (audience).
async function fetchWithAuth(url, options = {}) {
let token = getAccessToken();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
if (response.status === 401) {
// Try to refresh token
token = await refreshAccessToken();
if (token) {
// Retry with new token
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
} else {
// Refresh failed, redirect to login
redirectToLogin();
}
}
return response;
}
For same-origin requests with HttpOnly cookies:
const response = await fetch('/api/users', {
credentials: 'include', // Send cookies
});
APIs return errors in various formats:
// Simple message
{ "error": "User not found" }
// With code
{ "code": "USER_NOT_FOUND", "message": "User not found" }
// Validation errors
{
"errors": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Minimum 8 characters" }
]
}
async function apiRequest(url, options) {
try {
const response = await fetch(url, options);
// Parse response body
let data;
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// Handle HTTP errors
if (!response.ok) {
throw new ApiError(response.status, data);
}
return data;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
// Network error
throw new NetworkError(error.message);
}
}
class ApiError extends Error {
constructor(status, data) {
super(data.message || `HTTP ${status}`);
this.status = status;
this.data = data;
}
}
class NetworkError extends Error {
constructor(message) {
super(message || 'Network error');
}
}
function getErrorMessage(error) {
if (error instanceof NetworkError) {
return 'Unable to connect. Check your internet connection.';
}
if (error instanceof ApiError) {
switch (error.status) {
case 400:
return error.data.message || 'Invalid request';
case 401:
return 'Please log in to continue';
case 403:
return 'You don\'t have permission for this action';
case 404:
return 'Resource not found';
case 429:
return 'Too many requests. Please wait a moment.';
case 500:
case 502:
case 503:
return 'Server error. Please try again later.';
default:
return 'An unexpected error occurred';
}
}
return 'An unexpected error occurred';
}
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function useUsers() {
const [state, setState] = useState<RequestState<User[]>>({ status: 'idle' });
async function fetchUsers() {
setState({ status: 'loading' });
try {
const users = await apiRequest('/api/users');
setState({ status: 'success', data: users });
} catch (error) {
setState({ status: 'error', error });
}
}
return { state, fetchUsers };
}
| Symptom | Likely cause |
|---|---|
| Request pending forever | Server not responding, wrong URL |
| CORS error | Backend missing CORS headers |
| 401 on valid token | Token expired, wrong header format |
| Empty response body | Check Content-Type, parse method |
| Data shape mismatch | API changed, check docs |
For sharing requests with backend developers or testing:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ..." \
-d '{"name": "Ada", "email": "ada@example.com"}'
Generate cURL commands from request parameters with the cURL Command Generator.
Before sending or after receiving:
When your frontend (app.example.com) calls an API (api.example.com), CORS rules apply.
Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy
Fix: Backend must add CORS headers:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
For development, use a proxy or configure your dev server to forward API requests.
| Library | Use case |
|---|---|
| TanStack Query | Caching, background refetch, optimistic updates |
| SWR | Similar to TanStack Query, smaller |
| Axios | Request/response interceptors, better defaults |
| ky | Fetch wrapper with retries, simpler API |
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => apiRequest('/api/users'),
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data) => apiRequest('/api/users', {
method: 'POST',
body: JSON.stringify(data),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
APIs evolve. Versioning prevents breaking changes:
/api/v1/users
/api/v2/users
Or via headers:
Accept: application/vnd.api+json; version=2
Check API documentation for versioning strategy.
APIs limit requests to prevent abuse:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1625000000
Handle gracefully:
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
await delay(parseInt(retryAfter) * 1000);
return retry(request);
}
REST APIs are HTTP with conventions. Use the right method for the action. Check status codes, don't assume success. Parse JSON safely. Handle errors with user-friendly messages. Use DevTools Network tab to debug. Cache and deduplicate requests in complex UIs. Understand CORS before blaming the backend. Document your API contracts — TypeScript interfaces are living documentation.
Get Content-Type right for APIs, file uploads, and static assets — charset parameters, multipart boundaries, sniffing risks, and a practical MIME lookup workflow.
Turn OpenAPI operations into accurate curl commands, attach auth headers correctly, and use snippets to reproduce frontend API failures for backend teammates.
Understand browser CORS, OPTIONS preflight, Access-Control-Allow-Origin, and credentials mode — plus the SPA misconfigurations that waste hours in production debugging.
Find a developer tool