Lesson 4.3: Headers and Body
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Understand what HTTP headers are and why they matter
- Work with common request and response headers
- Use query parameters to filter and paginate data
- Send JSON data in request bodies
- Handle different content types
- Debug requests using browser developer tools
Introduction
HTTP requests and responses are like letters sent through the mail. The body is the main content (the letter itself), while headers are like the envelope - they contain metadata like sender, recipient, date, and special handling instructions.
Understanding headers, query parameters, and request bodies is essential for working with APIs effectively.
HTTP Headers
Headers are key-value pairs that provide metadata about the request or response.
Header-Name: Header-Value
Request Headers
Sent by the client to the server.
const response = await fetch('https://api.example.com/users', {
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token123',
Accept: 'application/json',
'Accept-Language': 'en-US',
},
});
Response Headers
Sent by the server back to the client.
const response = await fetch('https://api.example.com/users');
// Access response headers
console.log(response.headers.get('Content-Type')); // "application/json"
console.log(response.headers.get('X-RateLimit-Remaining')); // "99"
Common Request Headers
Content-Type
Tells the server what format the request body is in.
// JSON data
headers: {
"Content-Type": "application/json"
}
// Form data
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
// File upload
headers: {
"Content-Type": "multipart/form-data"
}
Accept
Tells the server what format you want the response in.
headers: {
"Accept": "application/json" // I want JSON back
}
// Accept multiple formats with preference
headers: {
"Accept": "application/json, text/plain, */*"
}
Authorization
Provides credentials for authentication.
// Bearer token (JWT, OAuth)
headers: {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
// Basic auth (username:password in base64)
headers: {
"Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
}
// API key (some APIs use custom headers)
headers: {
"X-API-Key": "your-api-key-here"
}
User-Agent
Identifies the client making the request.
headers: {
"User-Agent": "MyApp/1.0.0"
}
Accept-Language
Specifies preferred language for the response.
headers: {
"Accept-Language": "en-US,en;q=0.9,ru;q=0.8"
}
Common Response Headers
Content-Type
Tells you the format of the response body.
const response = await fetch('https://api.example.com/data');
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
const data = await response.json();
} else if (contentType?.includes('text/')) {
const text = await response.text();
}
Content-Length
Size of the response body in bytes.
const size = response.headers.get('Content-Length');
console.log(`Response size: ${size} bytes`);
Cache-Control
Instructions for caching.
const cacheControl = response.headers.get('Cache-Control');
// "max-age=3600" means cache for 1 hour
// "no-cache" means always revalidate
// "no-store" means never cache
Rate Limiting Headers
Many APIs include rate limit information.
const remaining = response.headers.get('X-RateLimit-Remaining');
const limit = response.headers.get('X-RateLimit-Limit');
const reset = response.headers.get('X-RateLimit-Reset');
console.log(`Requests remaining: ${remaining}/${limit}`);
console.log(`Limit resets at: ${new Date(Number(reset) * 1000)}`);
Set-Cookie
Server sets cookies (handled automatically by browsers).
Query Parameters
Query parameters are added to the URL to filter, sort, or paginate data.
https://api.example.com/users?role=admin&sort=name&limit=10
└─────────────┬─────────────┘
Query Parameters
Building URLs with Query Parameters
Method 1: String Concatenation (Not Recommended)
// Fragile and error-prone
const url = 'https://api.example.com/users?role=' + role + '&limit=' + limit;
Method 2: Template Literals (Better)
const url = `https://api.example.com/users?role=${role}&limit=${limit}`;
Method 3: URLSearchParams (Best)
const params = new URLSearchParams({
role: 'admin',
sort: 'name',
limit: '10',
});
const url = `https://api.example.com/users?${params}`;
// https://api.example.com/users?role=admin&sort=name&limit=10
URLSearchParams Features
const params = new URLSearchParams();
// Add parameters
params.append('tag', 'javascript');
params.append('tag', 'typescript'); // Can have multiple values
// Set a parameter (replaces existing)
params.set('limit', '20');
// Delete a parameter
params.delete('tag');
// Check if parameter exists
params.has('limit'); // true
// Get parameter value
params.get('limit'); // "20"
// Get all values for a key
params.getAll('tag'); // ["javascript", "typescript"]
// Convert to string
params.toString(); // "limit=20"
Common Query Parameter Patterns
// Pagination
const params = new URLSearchParams({
page: "2",
per_page: "25"
});
// ?page=2&per_page=25
// Filtering
const params = new URLSearchParams({
status: "active",
role: "admin",
created_after: "2024-01-01"
});
// ?status=active&role=admin&created_after=2024-01-01
// Sorting
const params = new URLSearchParams({
sort: "created_at",
order: "desc"
});
// ?sort=created_at&order=desc
// Searching
const params = new URLSearchParams({
q: "search term",
fields: "name,email"
});
// ?q=search+term&fields=name%2Cemail
Special Characters in Query Parameters
URLSearchParams automatically handles encoding:
const params = new URLSearchParams({
query: 'hello world', // Space becomes +
tag: 'c++', // + becomes %2B
filter: 'name=John&age>30', // Special chars encoded
});
console.log(params.toString());
// query=hello+world&tag=c%2B%2B&filter=name%3DJohn%26age%3E30
Request Body
The request body contains data you send to the server (POST, PUT, PATCH).
JSON Body (Most Common)
interface CreateUserRequest {
name: string;
email: string;
role: 'user' | 'admin';
}
const userData: CreateUserRequest = {
name: 'John Doe',
email: 'john@example.com',
role: 'user',
};
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
Form Data
For traditional form submissions or file uploads:
// URL-encoded form data
const formData = new URLSearchParams({
username: 'john',
password: 'secret123',
});
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData,
});
FormData Object (File Uploads)
const formData = new FormData();
formData.append('name', 'John');
formData.append('avatar', fileInput.files[0]); // File object
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
// Don't set Content-Type - browser sets it with boundary
body: formData,
});
Practical Examples
Example 1: Authenticated Request with Pagination
interface User {
id: number;
name: string;
email: string;
}
interface PaginatedResponse<T> {
data: T[];
page: number;
totalPages: number;
totalItems: number;
}
async function getUsers(
page: number = 1,
limit: number = 10,
token: string
): Promise<PaginatedResponse<User>> {
const params = new URLSearchParams({
page: String(page),
limit: String(limit),
});
const response = await fetch(`https://api.example.com/users?${params}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
// Usage
const result = await getUsers(2, 25, 'my-auth-token');
console.log(`Page ${result.page} of ${result.totalPages}`);
console.log(`Showing ${result.data.length} of ${result.totalItems} users`);
Example 2: Creating a Resource with JSON Body
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
interface CreatePostData {
title: string;
body: string;
userId: number;
}
async function createPost(postData: CreatePostData): Promise<Post> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(postData),
});
if (!response.ok) {
throw new Error(`Failed to create post: ${response.status}`);
}
// Server returns the created post with ID
return response.json();
}
// Usage
const newPost = await createPost({
title: 'My First Post',
body: 'This is the content of my post.',
userId: 1,
});
console.log(`Created post with ID: ${newPost.id}`);
Example 3: Search with Multiple Filters
interface SearchOptions {
query?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
sortBy?: 'price' | 'rating' | 'date';
order?: 'asc' | 'desc';
}
async function searchProducts(options: SearchOptions): Promise<Product[]> {
const params = new URLSearchParams();
// Only add parameters that have values
if (options.query) params.append('q', options.query);
if (options.category) params.append('category', options.category);
if (options.minPrice !== undefined) params.append('min_price', String(options.minPrice));
if (options.maxPrice !== undefined) params.append('max_price', String(options.maxPrice));
if (options.sortBy) params.append('sort', options.sortBy);
if (options.order) params.append('order', options.order);
const url = `https://api.example.com/products?${params}`;
console.log('Fetching:', url);
const response = await fetch(url);
return response.json();
}
// Usage
const results = await searchProducts({
query: 'laptop',
category: 'electronics',
minPrice: 500,
maxPrice: 1500,
sortBy: 'price',
order: 'asc',
});
Example 4: Handling Different Response Types
async function fetchResource(url: string): Promise<string | object | Blob> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
return response.json();
} else if (contentType.includes('text/')) {
return response.text();
} else if (contentType.includes('image/') || contentType.includes('application/pdf')) {
return response.blob();
}
// Default to text
return response.text();
}
Debugging HTTP Requests
Browser Developer Tools
- Open DevTools (F12 or right-click > Inspect)
- Go to the Network tab
- Make your request
- Click on the request to see:
- Headers (request and response)
- Payload (request body)
- Preview/Response (response body)
- Timing (how long each phase took)
Logging Requests
async function fetchWithLogging(url: string, options?: RequestInit): Promise<Response> {
console.group(`HTTP ${options?.method || 'GET'} ${url}`);
if (options?.headers) {
console.log('Request Headers:', options.headers);
}
if (options?.body) {
console.log('Request Body:', options.body);
}
const startTime = performance.now();
const response = await fetch(url, options);
const duration = performance.now() - startTime;
console.log(`Status: ${response.status} ${response.statusText}`);
console.log(`Duration: ${duration.toFixed(2)}ms`);
console.log('Response Headers:');
response.headers.forEach((value, key) => {
console.log(` ${key}: ${value}`);
});
console.groupEnd();
return response;
}
Common Mistakes
Mistake 1: Forgetting to Stringify JSON
// WRONG - Sends "[object Object]"
body: {
name: 'John';
}
// CORRECT
body: JSON.stringify({ name: 'John' });
Mistake 2: Wrong Content-Type for JSON
// WRONG
headers: {
"Content-Type": "text/plain" // Server won't parse as JSON
}
// CORRECT
headers: {
"Content-Type": "application/json"
}
Mistake 3: Not Encoding Query Parameters
// WRONG - Breaks if name contains special characters
const url = `https://api.example.com/users?name=${name}`;
// CORRECT
const params = new URLSearchParams({ name });
const url = `https://api.example.com/users?${params}`;
Mistake 4: Setting Content-Type for FormData
// WRONG - Overrides the boundary that browser needs to set
headers: {
"Content-Type": "multipart/form-data"
}
// CORRECT - Let the browser set it automatically
// Just don't include Content-Type header
Exercises
Exercise 1: Build a Search URL
Create a function that builds a search URL with the following parameters:
- query (required): search term
- page (optional, default 1): page number
- limit (optional, default 20): items per page
- sort (optional): field to sort by
function buildSearchUrl(baseUrl: string, options: SearchOptions): string {}
// Test
buildSearchUrl('https://api.example.com/search', {
query: 'typescript',
page: 2,
sort: 'relevance',
});
// Should return: https://api.example.com/search?query=typescript&page=2&limit=20&sort=relevance
Solution
interface SearchOptions {
query: string;
page?: number;
limit?: number;
sort?: string;
}
function buildSearchUrl(baseUrl: string, options: SearchOptions): string {
const params = new URLSearchParams({
query: options.query,
page: String(options.page ?? 1),
limit: String(options.limit ?? 20),
});
if (options.sort) {
params.append('sort', options.sort);
}
return `${baseUrl}?${params}`;
}
Exercise 2: Extract Rate Limit Info
Write a function that extracts rate limit information from response headers:
interface RateLimitInfo {
limit: number;
remaining: number;
resetsAt: Date;
}
function extractRateLimitInfo(response: Response): RateLimitInfo | null {}
Solution
interface RateLimitInfo {
limit: number;
remaining: number;
resetsAt: Date;
}
function extractRateLimitInfo(response: Response): RateLimitInfo | null {
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (!limit || !remaining || !reset) {
return null;
}
return {
limit: parseInt(limit, 10),
remaining: parseInt(remaining, 10),
resetsAt: new Date(parseInt(reset, 10) * 1000),
};
}
Exercise 3: Create a Request Builder
Create a class that helps build fetch requests:
class RequestBuilder {
// Implement methods:
// - setMethod(method)
// - setHeader(name, value)
// - setJsonBody(data)
// - addQueryParam(name, value)
// - build() -> returns { url, options }
}
// Usage:
const { url, options } = new RequestBuilder('https://api.example.com/users')
.setMethod('POST')
.setHeader('Authorization', 'Bearer token')
.setJsonBody({ name: 'John' })
.addQueryParam('notify', 'true')
.build();
Solution
class RequestBuilder {
private baseUrl: string;
private method: string = 'GET';
private headers: Record<string, string> = {};
private body?: string;
private params: URLSearchParams = new URLSearchParams();
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
setMethod(method: string): this {
this.method = method;
return this;
}
setHeader(name: string, value: string): this {
this.headers[name] = value;
return this;
}
setJsonBody(data: unknown): this {
this.headers['Content-Type'] = 'application/json';
this.body = JSON.stringify(data);
return this;
}
addQueryParam(name: string, value: string): this {
this.params.append(name, value);
return this;
}
build(): { url: string; options: RequestInit } {
const queryString = this.params.toString();
const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
const options: RequestInit = {
method: this.method,
headers: this.headers,
};
if (this.body) {
options.body = this.body;
}
return { url, options };
}
}
Key Takeaways
- Headers provide metadata about requests and responses
- Content-Type tells what format the body is in (usually
application/json) - Authorization header carries authentication tokens
- Use URLSearchParams to safely build query parameters
- Always JSON.stringify() objects before sending as body
- Check response headers for rate limits, caching info, and content type
- Use browser DevTools Network tab to debug HTTP requests
- Do not set Content-Type manually when using FormData
Resources
| Resource | Type | Level |
|---|---|---|
| MDN: HTTP Headers | Documentation | Beginner |
| MDN: URLSearchParams | Documentation | Beginner |
| HTTP Header Fields (Wikipedia) | Reference | Intermediate |
| Content-Type Header Explained | Documentation | Beginner |
Next Lesson
Now that you understand headers, parameters, and bodies, let us put it all together and learn how to use the Fetch API.