From Zero to AI

Lesson 4.3: Headers and Body

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Understand what HTTP headers are and why they matter
  2. Work with common request and response headers
  3. Use query parameters to filter and paginate data
  4. Send JSON data in request bodies
  5. Handle different content types
  6. 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)}`);

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

// 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

  1. Open DevTools (F12 or right-click > Inspect)
  2. Go to the Network tab
  3. Make your request
  4. 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

  1. Headers provide metadata about requests and responses
  2. Content-Type tells what format the body is in (usually application/json)
  3. Authorization header carries authentication tokens
  4. Use URLSearchParams to safely build query parameters
  5. Always JSON.stringify() objects before sending as body
  6. Check response headers for rate limits, caching info, and content type
  7. Use browser DevTools Network tab to debug HTTP requests
  8. 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.

Continue to Lesson 4.4: Fetch API