JSON & Working with APIs

50 min read Intermediate

Master JSON data format and learn to work with REST APIs, handling requests, responses, authentication, and error handling.

JSON Basics

JSON (JavaScript Object Notation) is a lightweight data format used for data exchange between client and server.

JSON
{
    "name": "John Doe",
    "age": 30,
    "isActive": true,
    "email": null,
    "hobbies": ["reading", "coding", "gaming"],
    "address": {
        "street": "123 Main St",
        "city": "New York",
        "country": "USA"
    },
    "orders": [
        { "id": 1, "product": "Book", "price": 29.99 },
        { "id": 2, "product": "Laptop", "price": 999.99 }
    ]
}
JavaScript
// JSON.parse() - convert JSON string to JavaScript object
const jsonString = '{"name": "John", "age": 30}';
const obj = JSON.parse(jsonString);
console.log(obj.name); // "John"

// JSON.stringify() - convert JavaScript object to JSON string
const user = { name: "Jane", age: 25, active: true };
const json = JSON.stringify(user);
console.log(json); // '{"name":"Jane","age":25,"active":true}'

// Pretty print with indentation
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
// {
//   "name": "Jane",
//   "age": 25,
//   "active": true
// }

// Selective stringify with replacer
const filtered = JSON.stringify(user, ["name", "age"]);
console.log(filtered); // '{"name":"Jane","age":25}'

// Transform during stringify
const transformed = JSON.stringify(user, (key, value) => {
    if (typeof value === "string") {
        return value.toUpperCase();
    }
    return value;
});

// Handle dates (they become strings)
const data = { 
    created: new Date(),
    name: "Event"
};
console.log(JSON.stringify(data));
// {"created":"2024-01-15T10:30:00.000Z","name":"Event"}

// Parse with reviver (transform back)
const parsed = JSON.parse(json, (key, value) => {
    if (key === "created") {
        return new Date(value);
    }
    return value;
});

// Deep clone with JSON (loses functions, undefined, symbols)
const clone = JSON.parse(JSON.stringify(original));
JSON Limitations
  • Keys must be strings (double quotes)
  • No trailing commas allowed
  • No comments allowed
  • Functions, undefined, Symbol are not serialized
  • Dates become strings

Fetch API Basics

The Fetch API provides a modern way to make HTTP requests.

JavaScript
// Basic GET request
async function getUsers() {
    const response = await fetch("https://api.example.com/users");
    const data = await response.json();
    return data;
}

// With error handling
async function fetchData(url) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        return await response.json();
    } catch (error) {
        console.error("Fetch error:", error);
        throw error;
    }
}

// POST request
async function createUser(userData) {
    const response = await fetch("https://api.example.com/users", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(userData)
    });
    
    return response.json();
}

// PUT request (update)
async function updateUser(id, userData) {
    const response = await fetch(`https://api.example.com/users/${id}`, {
        method: "PUT",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(userData)
    });
    
    return response.json();
}

// PATCH request (partial update)
async function patchUser(id, changes) {
    const response = await fetch(`https://api.example.com/users/${id}`, {
        method: "PATCH",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(changes)
    });
    
    return response.json();
}

// DELETE request
async function deleteUser(id) {
    const response = await fetch(`https://api.example.com/users/${id}`, {
        method: "DELETE"
    });
    
    if (!response.ok) {
        throw new Error("Delete failed");
    }
    
    return true;
}

Request Options

Configure requests with headers, credentials, caching, and more.

JavaScript
// Full options object
const options = {
    method: "POST",           // GET, POST, PUT, PATCH, DELETE
    
    headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer token123",
        "Accept": "application/json",
        "X-Custom-Header": "value"
    },
    
    body: JSON.stringify(data), // String, FormData, Blob, etc.
    
    mode: "cors",              // cors, no-cors, same-origin
    credentials: "include",     // include, same-origin, omit
    cache: "no-cache",         // default, no-cache, reload, force-cache
    redirect: "follow",        // follow, error, manual
    
    signal: controller.signal  // AbortController signal
};

const response = await fetch(url, options);

// Using Headers object
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", "Bearer token");

// Working with response
console.log(response.ok);          // true if 200-299
console.log(response.status);      // 200
console.log(response.statusText);  // "OK"
console.log(response.url);         // Final URL (after redirects)
console.log(response.type);        // "basic", "cors", etc.

// Response headers
console.log(response.headers.get("Content-Type"));

// Iterate headers
for (const [key, value] of response.headers) {
    console.log(`${key}: ${value}`);
}

// Different response types
const json = await response.json();       // Parse as JSON
const text = await response.text();       // Get as string
const blob = await response.blob();       // Get as Blob
const buffer = await response.arrayBuffer(); // Get as ArrayBuffer
const formData = await response.formData(); // Get as FormData

// Check content type before parsing
const contentType = response.headers.get("Content-Type");
if (contentType?.includes("application/json")) {
    const data = await response.json();
} else {
    const text = await response.text();
}

Authentication

Handle API authentication with tokens and credentials.

JavaScript
// Bearer Token Authentication
async function fetchWithAuth(url, token) {
    return fetch(url, {
        headers: {
            "Authorization": `Bearer ${token}`
        }
    });
}

// API Key Authentication
async function fetchWithApiKey(url, apiKey) {
    return fetch(url, {
        headers: {
            "X-API-Key": apiKey
        }
    });
}

// Basic Authentication
async function fetchWithBasicAuth(url, username, password) {
    const credentials = btoa(`${username}:${password}`);
    return fetch(url, {
        headers: {
            "Authorization": `Basic ${credentials}`
        }
    });
}

// Login and store token
async function login(email, password) {
    const response = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password })
    });
    
    if (!response.ok) {
        throw new Error("Login failed");
    }
    
    const { token, user } = await response.json();
    
    // Store token (be careful with localStorage for sensitive data)
    localStorage.setItem("authToken", token);
    
    return user;
}

// Token refresh
async function refreshToken() {
    const refreshToken = localStorage.getItem("refreshToken");
    
    const response = await fetch("/api/auth/refresh", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ refreshToken })
    });
    
    if (!response.ok) {
        // Redirect to login
        window.location.href = "/login";
        return;
    }
    
    const { token } = await response.json();
    localStorage.setItem("authToken", token);
    return token;
}

// Authenticated fetch wrapper
async function authFetch(url, options = {}) {
    const token = localStorage.getItem("authToken");
    
    const response = await fetch(url, {
        ...options,
        headers: {
            ...options.headers,
            "Authorization": `Bearer ${token}`
        }
    });
    
    // Handle token expiration
    if (response.status === 401) {
        const newToken = await refreshToken();
        
        // Retry with new token
        return fetch(url, {
            ...options,
            headers: {
                ...options.headers,
                "Authorization": `Bearer ${newToken}`
            }
        });
    }
    
    return response;
}

Error Handling

Handle different types of errors when working with APIs.

JavaScript
// Custom API Error class
class ApiError extends Error {
    constructor(message, status, data = null) {
        super(message);
        this.name = "ApiError";
        this.status = status;
        this.data = data;
    }
}

// Comprehensive fetch wrapper
async function apiFetch(url, options = {}) {
    try {
        const response = await fetch(url, {
            ...options,
            headers: {
                "Content-Type": "application/json",
                ...options.headers
            }
        });
        
        // Try to parse JSON (might fail for empty responses)
        let data;
        const contentType = response.headers.get("Content-Type");
        if (contentType?.includes("application/json")) {
            data = await response.json();
        }
        
        // Handle HTTP errors
        if (!response.ok) {
            throw new ApiError(
                data?.message || response.statusText,
                response.status,
                data
            );
        }
        
        return data;
        
    } catch (error) {
        // Network error (no response at all)
        if (error instanceof TypeError) {
            throw new ApiError("Network error - check your connection", 0);
        }
        
        // Re-throw ApiError
        if (error instanceof ApiError) {
            throw error;
        }
        
        // Unknown error
        throw new ApiError("An unexpected error occurred", 500);
    }
}

// Usage with specific error handling
async function loadUserProfile(userId) {
    try {
        const user = await apiFetch(`/api/users/${userId}`);
        return user;
        
    } catch (error) {
        if (error instanceof ApiError) {
            switch (error.status) {
                case 401:
                    redirectToLogin();
                    break;
                case 403:
                    showError("You don't have permission to view this");
                    break;
                case 404:
                    showError("User not found");
                    break;
                case 429:
                    showError("Too many requests, please wait");
                    break;
                case 500:
                    showError("Server error, please try again");
                    break;
                default:
                    showError(error.message);
            }
        }
        
        return null;
    }
}

// Retry with exponential backoff
async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            return await apiFetch(url, options);
        } catch (error) {
            const isLastAttempt = i === retries - 1;
            const shouldRetry = error.status >= 500 || error.status === 0;
            
            if (isLastAttempt || !shouldRetry) {
                throw error;
            }
            
            // Wait with exponential backoff
            const delay = Math.pow(2, i) * 1000;
            await new Promise(r => setTimeout(r, delay));
        }
    }
}

API Client Patterns

Build reusable API clients for organized code.

JavaScript
// API Client class
class ApiClient {
    constructor(baseUrl, options = {}) {
        this.baseUrl = baseUrl;
        this.defaultHeaders = options.headers || {};
        this.timeout = options.timeout || 30000;
    }
    
    async request(endpoint, options = {}) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);
        
        try {
            const response = await fetch(`${this.baseUrl}${endpoint}`, {
                ...options,
                headers: {
                    "Content-Type": "application/json",
                    ...this.defaultHeaders,
                    ...options.headers
                },
                signal: controller.signal
            });
            
            clearTimeout(timeoutId);
            
            if (!response.ok) {
                const error = await response.json().catch(() => ({}));
                throw new ApiError(error.message, response.status);
            }
            
            return response.json();
        } catch (error) {
            clearTimeout(timeoutId);
            throw error;
        }
    }
    
    get(endpoint, options) {
        return this.request(endpoint, { ...options, method: "GET" });
    }
    
    post(endpoint, data, options) {
        return this.request(endpoint, {
            ...options,
            method: "POST",
            body: JSON.stringify(data)
        });
    }
    
    put(endpoint, data, options) {
        return this.request(endpoint, {
            ...options,
            method: "PUT",
            body: JSON.stringify(data)
        });
    }
    
    patch(endpoint, data, options) {
        return this.request(endpoint, {
            ...options,
            method: "PATCH",
            body: JSON.stringify(data)
        });
    }
    
    delete(endpoint, options) {
        return this.request(endpoint, { ...options, method: "DELETE" });
    }
    
    setHeader(key, value) {
        this.defaultHeaders[key] = value;
    }
}

// Create API client instance
const api = new ApiClient("https://api.example.com", {
    headers: {
        "Authorization": `Bearer ${getToken()}`
    }
});

// Resource-specific modules
const usersApi = {
    getAll: () => api.get("/users"),
    getById: (id) => api.get(`/users/${id}`),
    create: (data) => api.post("/users", data),
    update: (id, data) => api.put(`/users/${id}`, data),
    delete: (id) => api.delete(`/users/${id}`)
};

const postsApi = {
    getAll: (params) => api.get(`/posts?${new URLSearchParams(params)}`),
    getById: (id) => api.get(`/posts/${id}`),
    create: (data) => api.post("/posts", data),
    update: (id, data) => api.put(`/posts/${id}`, data),
    delete: (id) => api.delete(`/posts/${id}`)
};

// Usage
const users = await usersApi.getAll();
const newUser = await usersApi.create({ name: "John" });
const posts = await postsApi.getAll({ page: 1, limit: 10 });

WebSocket API

WebSocket provides full-duplex communication over a single TCP connection, enabling real-time features like chat, live updates, and multiplayer games.

Basic WebSocket Connection

JavaScript - WebSocket Basics
// Create WebSocket connection
const socket = new WebSocket('wss://api.example.com/ws');

// Connection opened
socket.addEventListener('open', (event) => {
    console.log('Connected to WebSocket server');
    
    // Send a message
    socket.send(JSON.stringify({
        type: 'subscribe',
        channel: 'notifications'
    }));
});

// Listen for messages
socket.addEventListener('message', (event) => {
    const data = JSON.parse(event.data);
    console.log('Received:', data);
    
    switch (data.type) {
        case 'notification':
            showNotification(data.payload);
            break;
        case 'update':
            updateUI(data.payload);
            break;
    }
});

// Handle errors
socket.addEventListener('error', (error) => {
    console.error('WebSocket error:', error);
});

// Connection closed
socket.addEventListener('close', (event) => {
    console.log('Disconnected:', event.code, event.reason);
    
    if (event.code !== 1000) { // Abnormal closure
        // Attempt to reconnect
        setTimeout(() => reconnect(), 3000);
    }
});

// Send data
function send(type, payload) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type, payload }));
    }
}

// Close connection
function disconnect() {
    socket.close(1000, 'User disconnected');
}

Robust WebSocket Client

JavaScript - WebSocket with Reconnection
class WebSocketClient {
    constructor(url, options = {}) {
        this.url = url;
        this.options = {
            reconnectInterval: 3000,
            maxReconnectAttempts: 5,
            heartbeatInterval: 30000,
            ...options
        };
        
        this.socket = null;
        this.reconnectAttempts = 0;
        this.listeners = new Map();
        this.heartbeatTimer = null;
    }
    
    connect() {
        this.socket = new WebSocket(this.url);
        
        this.socket.onopen = () => {
            console.log('WebSocket connected');
            this.reconnectAttempts = 0;
            this.startHeartbeat();
            this.emit('connected');
        };
        
        this.socket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            
            if (data.type === 'pong') return; // Heartbeat response
            
            this.emit('message', data);
            this.emit(data.type, data.payload);
        };
        
        this.socket.onclose = (event) => {
            this.stopHeartbeat();
            this.emit('disconnected', event);
            
            if (event.code !== 1000 && this.reconnectAttempts < this.options.maxReconnectAttempts) {
                this.scheduleReconnect();
            }
        };
        
        this.socket.onerror = (error) => {
            this.emit('error', error);
        };
    }
    
    scheduleReconnect() {
        this.reconnectAttempts++;
        const delay = this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
        
        console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
        setTimeout(() => this.connect(), delay);
    }
    
    startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
            this.send('ping', {});
        }, this.options.heartbeatInterval);
    }
    
    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }
    
    send(type, payload) {
        if (this.socket?.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify({ type, payload }));
        }
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
    }
    
    emit(event, data) {
        this.listeners.get(event)?.forEach(cb => cb(data));
    }
    
    disconnect() {
        this.stopHeartbeat();
        this.socket?.close(1000);
    }
}

// Usage
const ws = new WebSocketClient('wss://api.example.com/ws');

ws.on('connected', () => ws.send('subscribe', { channel: 'updates' }));
ws.on('notification', (data) => showNotification(data));
ws.on('error', (err) => console.error('WS Error:', err));

ws.connect();

GraphQL Introduction

GraphQL is a query language for APIs that lets clients request exactly the data they need. Unlike REST, a single endpoint handles all requests.

GraphQL Queries

JavaScript - GraphQL Client
// Simple GraphQL client
async function graphql(query, variables = {}) {
    const response = await fetch('https://api.example.com/graphql', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${getToken()}`
        },
        body: JSON.stringify({ query, variables })
    });
    
    const { data, errors } = await response.json();
    
    if (errors) {
        throw new Error(errors.map(e => e.message).join(', '));
    }
    
    return data;
}

// Query - Get specific fields
const userData = await graphql(`
    query GetUser($id: ID!) {
        user(id: $id) {
            id
            name
            email
            posts {
                id
                title
                createdAt
            }
        }
    }
`, { id: '123' });

// Mutation - Create data
const newPost = await graphql(`
    mutation CreatePost($input: PostInput!) {
        createPost(input: $input) {
            id
            title
            author {
                name
            }
        }
    }
`, {
    input: {
        title: 'My New Post',
        content: 'Post content here...',
        published: true
    }
});

// Fragment - Reusable field selections
const postsWithUser = await graphql(`
    fragment UserFields on User {
        id
        name
        avatar
    }
    
    query GetPosts {
        posts {
            id
            title
            author {
                ...UserFields
            }
        }
    }
`);
REST vs GraphQL
  • REST: Multiple endpoints, fixed data structure, potential over/under-fetching
  • GraphQL: Single endpoint, request exactly what you need, strongly typed schema
  • Choose REST for: Simple CRUD, caching needs, public APIs
  • Choose GraphQL for: Complex data relationships, mobile apps, flexible UIs

Server-Sent Events (SSE)

SSE provides a simple way to receive real-time updates from a server over HTTP. Unlike WebSocket, it's unidirectional (server to client only) but simpler to implement.

JavaScript - Server-Sent Events
// Connect to SSE endpoint
const eventSource = new EventSource('/api/events', {
    withCredentials: true // Include cookies
});

// Default message event
eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Message:', data);
};

// Named events
eventSource.addEventListener('notification', (event) => {
    const notification = JSON.parse(event.data);
    showNotification(notification);
});

eventSource.addEventListener('update', (event) => {
    const update = JSON.parse(event.data);
    updateDashboard(update);
});

// Connection opened
eventSource.onopen = () => {
    console.log('SSE connection established');
};

// Handle errors
eventSource.onerror = (error) => {
    console.error('SSE error:', error);
    
    if (eventSource.readyState === EventSource.CLOSED) {
        console.log('Connection closed, will auto-reconnect');
    }
};

// Close connection
function disconnect() {
    eventSource.close();
}

// SSE with custom headers (using fetch + ReadableStream)
async function sseWithHeaders(url, headers) {
    const response = await fetch(url, { headers });
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        
        const text = decoder.decode(value);
        const lines = text.split('\n');
        
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = JSON.parse(line.slice(6));
                handleEvent(data);
            }
        }
    }
}

Summary

JSON.parse

Convert JSON string to JavaScript object

JSON.stringify

Convert object to JSON string

Fetch API

Modern HTTP requests with Promises

Response Handling

Check response.ok, parse appropriately

Authentication

Bearer tokens, API keys, refresh flow

Error Handling

Custom errors, retry logic, timeouts