JSON and API Integration
Why JSON and API Integration Matters
Modern applications integrate with external APIs for payments, authentication, data enrichment, and third-party services. Production API clients require error handling, retry logic, timeout management, request validation, and response transformation to handle network failures, rate limits, and malformed data.
Core Benefits:
- Service composition: Combine multiple services into cohesive application
- Separation of concerns: Frontend/backend/services communicate via APIs
- Third-party integration: Payment processors, email services, analytics
- Microservices: Services communicate via HTTP APIs
- Data exchange: JSON is universal data interchange format
Problem: Network requests fail unpredictably, APIs rate limit, responses malformed, and manual error handling is verbose and error-prone.
Solution: Use production HTTP clients (fetch, axios, got) with automatic retry, timeout handling, request/response interceptors, and schema validation (Zod) to ensure type safety and data integrity.
Standard Library First: JSON.parse and JSON.stringify
Node.js provides JSON parsing and serialization without external dependencies.
JSON Serialization and Parsing
Convert JavaScript objects to JSON strings and vice versa.
Pattern:
// Object to JSON string (serialization)
const user = {
// => JavaScript object
id: 123,
name: "Alice",
email: "alice@example.com",
tags: ["developer", "typescript"],
metadata: {
lastLogin: new Date(),
premium: true,
},
};
const jsonString = JSON.stringify(user);
// => Convert object to JSON string
// => Returns: '{"id":123,"name":"Alice","email":"alice@example.com",...}'
console.log(jsonString);
// Pretty-print JSON (with indentation)
const prettyJson = JSON.stringify(user, null, 2);
// => First parameter: object
// => Second parameter: replacer function (null = include all)
// => Third parameter: indentation spaces (2)
console.log(prettyJson);
/*
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"tags": [
"developer",
"typescript"
],
"metadata": {
"lastLogin": "2026-02-07T00:00:00.000Z",
"premium": true
}
}
*/
// JSON string to object (parsing)
const parsedUser = JSON.parse(jsonString);
// => Parse JSON string to JavaScript object
// => Returns object with same structure
console.log(parsedUser.name);
// => "Alice"
console.log(typeof parsedUser.metadata.lastLogin);
// => "string" (NOT Date object)
// => JSON.parse doesn't restore Date objects
// Custom serialization (replacer function)
const jsonWithReplacer = JSON.stringify(
user,
(key, value) => {
// => Replacer function for each property
// => key: Property name
// => value: Property value
if (key === "email") {
// => Exclude email from serialization
return undefined;
// => undefined values excluded from JSON
}
if (value instanceof Date) {
// => Custom Date serialization
return value.toISOString();
}
return value;
},
2,
);
// => email excluded, Date converted to ISO string
// Custom parsing (reviver function)
const jsonWithDates = '{"name":"Alice","createdAt":"2026-02-07T00:00:00.000Z"}';
const parsedWithReviver = JSON.parse(jsonWithDates, (key, value) => {
// => Reviver function for each property
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
// => Check if value looks like ISO date string
return new Date(value);
// => Convert string to Date object
}
return value;
});
console.log(parsedWithReviver.createdAt instanceof Date);
// => true (restored to Date object)
HTTP Requests with Node.js http/https
Built-in HTTP client for making requests.
Pattern:
import https from "https";
// => Built-in HTTPS module
// => No external dependencies
function httpsGet(url: string): Promise<string> {
// => Promise-based wrapper for https.get
// => Returns response body as string
return new Promise((resolve, reject) => {
// => Create Promise for async operation
https
.get(url, (res) => {
// => Make GET request
// => res: IncomingMessage (response stream)
let data = "";
// => Buffer for response body
res.on("data", (chunk) => {
// => Data event: Chunk of response received
data += chunk;
// => Append chunk to buffer
});
res.on("end", () => {
// => End event: Response complete
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
// => Check status code
// => 2xx = success
resolve(data);
// => Return response body
} else {
// => Non-success status code
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
}
});
})
.on("error", (error) => {
// => Error event: Network error
reject(error);
});
});
}
// Usage
try {
const response = await httpsGet("https://api.github.com/users/octocat");
// => Make GET request
const user = JSON.parse(response);
// => Parse JSON response
console.log(`GitHub user: ${user.login}`);
} catch (error) {
console.error("Request failed:", error);
}
// POST request with https.request
function httpsPost(url: string, data: any): Promise<string> {
// => POST request with JSON body
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
// => Parse URL to components
// => hostname, port, pathname
const postData = JSON.stringify(data);
// => Serialize request body to JSON
const options = {
hostname: urlObj.hostname,
port: urlObj.port || 443,
// => HTTPS default port: 443
path: urlObj.pathname + urlObj.search,
// => Path with query string
method: "POST",
headers: {
"Content-Type": "application/json",
// => JSON content type
"Content-Length": Buffer.byteLength(postData),
// => Content length in bytes
},
};
const req = https.request(options, (res) => {
// => Create POST request
let responseData = "";
res.on("data", (chunk) => {
responseData += chunk;
});
res.on("end", () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(responseData);
} else {
reject(new Error(`HTTP ${res.statusCode}`));
}
});
});
req.on("error", (error) => {
reject(error);
});
req.write(postData);
// => Write request body
req.end();
// => Send request
});
}Limitations for production:
- Verbose API: Manual stream handling, Promise wrapping required
- No timeout handling: Requests hang indefinitely if server unresponsive
- No retry logic: Network failures require manual retry implementation
- No request interceptors: Cannot add auth headers globally
- No automatic JSON parsing: Must manually call JSON.parse
- Limited error handling: HTTP error codes not rejected automatically
- No request cancellation: Cannot abort in-flight requests
- No progress tracking: Cannot track upload/download progress
When standard library suffices:
- Simple scripts (one-off API calls)
- No external dependencies requirement (embedded systems)
- Learning HTTP fundamentals
- Simple GET requests only
Production Framework: fetch API
Modern fetch API provides Promise-based HTTP requests (built into Node.js 18+).
Installation and Setup
// Node.js 18+ includes fetch globally
// No installation required
// For Node.js < 18:
// npm install node-fetch
// import fetch from 'node-fetch';
Basic usage:
// GET request
const response = await fetch("https://api.github.com/users/octocat");
// => Make GET request
// => Returns Response object (not response body)
if (!response.ok) {
// => Check if request successful
// => response.ok: true if status 200-299
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
// => Parse JSON response body
// => Automatically handles JSON.parse
console.log(`GitHub user: ${user.login}`);
// POST request with JSON body
const createResponse = await fetch("https://api.example.com/users", {
method: "POST",
// => HTTP method
headers: {
"Content-Type": "application/json",
// => JSON content type
Authorization: `Bearer ${token}`,
// => Authorization header
},
body: JSON.stringify({
// => Request body (must be string)
name: "Alice",
email: "alice@example.com",
}),
});
const newUser = await createResponse.json();
// => Parse response
// Query parameters
const searchParams = new URLSearchParams({
// => Build query string
q: "typescript",
sort: "stars",
order: "desc",
});
const searchResponse = await fetch(`https://api.github.com/search/repositories?${searchParams}`);
// => GET https://api.github.com/search/repositories?q=typescript&sort=stars&order=desc
const searchResults = await searchResponse.json();
// Error handling
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
// => Handle HTTP errors
// => 4xx, 5xx status codes
const errorBody = await response.text();
throw new Error(`HTTP ${response.status}: ${errorBody}`);
}
const data = await response.json();
} catch (error) {
// => Catch network errors and HTTP errors
console.error("Request failed:", error);
}Request Timeout
Abort requests that take too long.
Pattern:
// Timeout with AbortController
const controller = new AbortController();
// => Create abort controller
// => Controls request cancellation
const timeoutId = setTimeout(() => {
// => Set timeout
controller.abort();
// => Abort request after timeout
}, 5000);
// => 5 second timeout
try {
const response = await fetch("https://api.example.com/slow-endpoint", {
signal: controller.signal,
// => Pass abort signal to fetch
// => Allows request cancellation
});
clearTimeout(timeoutId);
// => Clear timeout if request completes
const data = await response.json();
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
// => Request aborted due to timeout
console.error("Request timeout");
} else {
console.error("Request failed:", error);
}
}
// Reusable timeout wrapper
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeout: number = 5000) {
// => Fetch with automatic timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// Usage
const response = await fetchWithTimeout("https://api.example.com/data", {}, 3000);
// => 3 second timeout
Production benefits:
- Promise-based: Async/await syntax (cleaner than callbacks)
- Built-in: No external dependencies (Node.js 18+)
- AbortController: Request cancellation support
- Streaming: Response body as stream (large files)
- Standard API: Same API as browser fetch
Trade-offs:
- No automatic retry: Must implement retry logic manually
- No request/response interceptors: Cannot add global headers easily
- Manual timeout handling: AbortController required for timeouts
- No progress events: Cannot track upload/download progress
- Limited error handling: Network errors only, HTTP errors not thrown
When to use fetch:
- Node.js 18+ applications (built-in)
- Simple API calls (GET, POST)
- No advanced features required (retry, interceptors)
- Browser-compatible code (same API)
Production Framework: Axios
Axios provides feature-rich HTTP client with interceptors, automatic retries, and progress tracking.
Installation and Setup
npm install axios
# => Install axios HTTP client
# => Production-ready with extensive featuresBasic usage:
import axios from "axios";
// => Import axios library
// GET request
const response = await axios.get("https://api.github.com/users/octocat");
// => Make GET request
// => Returns AxiosResponse object
console.log(response.data);
// => Response body (automatically parsed JSON)
// => response.data: Parsed JSON object
console.log(response.status);
// => HTTP status code (200)
console.log(response.headers);
// => Response headers object
// POST request
const createResponse = await axios.post("https://api.example.com/users", {
// => Request body object (automatically stringified)
name: "Alice",
email: "alice@example.com",
});
// => No manual JSON.stringify needed
// Query parameters (automatic encoding)
const searchResponse = await axios.get("https://api.github.com/search/repositories", {
params: {
// => Query parameters object
// => Automatically URL-encoded
q: "typescript",
sort: "stars",
order: "desc",
},
});
// => GET https://api.github.com/search/repositories?q=typescript&sort=stars&order=desc
// Custom headers
await axios.get("https://api.example.com/protected", {
headers: {
Authorization: `Bearer ${token}`,
// => Authorization header
"X-Custom-Header": "value",
},
});
// Error handling
try {
const response = await axios.get("https://api.example.com/data");
} catch (error) {
if (axios.isAxiosError(error)) {
// => Type guard for Axios errors
// => error is AxiosError (typed)
console.error("HTTP error:", error.response?.status);
// => HTTP status code (404, 500, etc.)
console.error("Error message:", error.message);
console.error("Response body:", error.response?.data);
// => Error response from server
} else {
console.error("Network error:", error);
}
}Request and Response Interceptors
Global request/response transformation and error handling.
Pattern:
// Request interceptor (add auth header to all requests)
axios.interceptors.request.use(
(config) => {
// => Modify request config before sending
// => config: AxiosRequestConfig
const token = localStorage.getItem("token");
// => Get auth token from storage
if (token) {
config.headers.Authorization = `Bearer ${token}`;
// => Add Authorization header to every request
// => No need to manually add to each request
}
console.log(`Request: ${config.method?.toUpperCase()} ${config.url}`);
// => Log outgoing requests
return config;
// => Return modified config
},
(error) => {
// => Handle request errors
return Promise.reject(error);
},
);
// Response interceptor (handle errors globally)
axios.interceptors.response.use(
(response) => {
// => Successful response (2xx status)
return response;
},
async (error) => {
// => Error response (4xx, 5xx status)
if (axios.isAxiosError(error) && error.response) {
const status = error.response.status;
if (status === 401) {
// => Unauthorized: Token expired or invalid
console.log("Token expired, refreshing...");
// Refresh token
const newToken = await refreshAuthToken();
localStorage.setItem("token", newToken);
// Retry original request with new token
error.config!.headers.Authorization = `Bearer ${newToken}`;
return axios.request(error.config!);
// => Retry request with new token
}
if (status === 429) {
// => Rate limited
const retryAfter = error.response.headers["retry-after"];
console.log(`Rate limited, retry after ${retryAfter} seconds`);
}
}
return Promise.reject(error);
},
);
// Usage (auth header added automatically)
const response = await axios.get("https://api.example.com/protected");
// => Authorization header added by interceptor
// => No need to manually add token
Timeout and Retry Configuration
Configure global timeout and retry logic.
Pattern:
// Create axios instance with default config
const api = axios.create({
// => Custom axios instance
// => Independent from global axios
baseURL: "https://api.example.com",
// => Base URL for all requests
// => axios.get('/users') → https://api.example.com/users
timeout: 5000,
// => Request timeout: 5 seconds
// => Aborts request if no response
headers: {
"Content-Type": "application/json",
// => Default headers for all requests
},
});
// Retry logic with axios-retry
import axiosRetry from "axios-retry";
// => Install: npm install axios-retry
axiosRetry(api, {
// => Enable retry on axios instance
retries: 3,
// => Retry up to 3 times
retryDelay: axiosRetry.exponentialDelay,
// => Exponential backoff: 1s, 2s, 4s
// => Alternative: axiosRetry.linearDelay (constant delay)
retryCondition: (error) => {
// => Determine if request should be retried
// => error: AxiosError
return (
axiosRetry.isNetworkOrIdempotentRequestError(error) ||
// => Retry on network errors
// => Retry on idempotent requests (GET, PUT, DELETE)
error.response?.status === 429
// => Retry on rate limit (429 status)
);
},
onRetry: (retryCount, error, requestConfig) => {
// => Called before each retry
console.log(`Retry attempt ${retryCount} for ${requestConfig.url}`);
},
});
// Usage (automatic timeout and retry)
try {
const response = await api.get("/users");
// => Automatically retries on network errors
// => Automatically times out after 5 seconds
} catch (error) {
// => All retries exhausted or non-retryable error
console.error("Request failed after retries:", error);
}Type-Safe API Client with Zod Validation
Validate API responses against schema.
Pattern:
import { z } from "zod";
import axios from "axios";
// Define response schema
const userSchema = z.object({
// => Expected API response structure
id: z.number(),
login: z.string(),
name: z.string().nullable(),
// => Nullable field (API may return null)
email: z.string().email().optional(),
// => Optional field (may not be present)
created_at: z.string().transform((str) => new Date(str)),
// => Transform string to Date object
});
type User = z.infer<typeof userSchema>;
// => Infer TypeScript type from schema
async function getGitHubUser(username: string): Promise<User> {
// => Type-safe API call
// => Returns validated User object
const response = await axios.get(`https://api.github.com/users/${username}`);
// => Make API request
try {
const user = userSchema.parse(response.data);
// => Validate response against schema
// => Throws ZodError if invalid
return user;
// => Return validated, typed user
} catch (error) {
if (error instanceof z.ZodError) {
// => Validation error
console.error("Invalid API response:", error.errors);
throw new Error(`Invalid API response: ${error.errors.map((e) => e.message).join(", ")}`);
}
throw error;
}
}
// Usage
const user = await getGitHubUser("octocat");
console.log(user.created_at instanceof Date);
// => true (transformed to Date)
console.log(user.login);
// => Type-safe access (TypeScript knows field exists)
// Generic API client
async function apiCall<T>(url: string, schema: z.ZodSchema<T>): Promise<T> {
// => Generic type-safe API call
// => T: Expected response type
const response = await axios.get(url);
return schema.parse(response.data);
// => Validate and return typed response
}
// Usage
const repositories = await apiCall(
"https://api.github.com/users/octocat/repos",
z.array(
z.object({
// => Array of repositories
id: z.number(),
name: z.string(),
stargazers_count: z.number(),
}),
),
);
// => Type-safe: repositories[0].stargazers_count is number
Production benefits:
- Automatic JSON parsing: response.data is parsed object
- Request/response interceptors: Global auth, logging, error handling
- Automatic retry: Exponential backoff for transient failures
- Timeout handling: Built-in timeout support
- Progress tracking: Upload/download progress events
- Request cancellation: AbortController integration
- TypeScript support: Strong typing with generics
Trade-offs:
- External dependency: Axios library (135KB)
- Bundle size: Larger than fetch (but minimal in server-side)
- Learning curve: Interceptors, instance configuration
When to use Axios:
- Production applications (always - robust error handling)
- Complex API integration (multiple endpoints)
- Retry logic required (transient failures)
- Global request configuration (auth headers, base URL)
Production Framework: Got
Got provides modern HTTP client with built-in retry, caching, and hooks.
Installation and Setup
npm install got
# => Install got HTTP client
# => Alternative to axios with different APIBasic usage:
import got from "got";
// => Import got library
// GET request
const response = await got.get("https://api.github.com/users/octocat");
// => Make GET request
// => Returns response object
const user = JSON.parse(response.body);
// => response.body is string (not parsed)
// Automatic JSON parsing
const user2 = await got.get("https://api.github.com/users/octocat").json<User>();
// => .json() parses response body
// => Generic type for TypeScript
console.log(user2.login);
// => Type-safe access
// POST request
const newUser = await got
.post("https://api.example.com/users", {
json: {
// => Request body as JSON
// => Automatically stringified and Content-Type set
name: "Alice",
email: "alice@example.com",
},
})
.json();
// => Parse response as JSON
// Query parameters
const searchResults = await got
.get("https://api.github.com/search/repositories", {
searchParams: {
// => Query parameters object
q: "typescript",
sort: "stars",
},
})
.json();
// Custom headers
await got.get("https://api.example.com/protected", {
headers: {
Authorization: `Bearer ${token}`,
},
});Built-in Retry and Timeout
Automatic retry with exponential backoff.
Pattern:
const api = got.extend({
// => Extend got with default options
// => Creates new got instance
prefixUrl: "https://api.example.com",
// => Base URL for all requests
// => api.get('users') → https://api.example.com/users
timeout: {
request: 5000,
// => Total request timeout: 5 seconds
// => Includes all retries
},
retry: {
limit: 3,
// => Retry up to 3 times
methods: ["GET", "PUT", "DELETE"],
// => Retry only idempotent methods
statusCodes: [408, 413, 429, 500, 502, 503, 504],
// => Retry on specific status codes
// => 429: Rate limit, 5xx: Server errors
errorCodes: ["ETIMEDOUT", "ECONNRESET", "EADDRINUSE"],
// => Retry on network errors
backoffLimit: 10000,
// => Maximum backoff: 10 seconds
},
hooks: {
beforeRequest: [
(options) => {
// => Hook before request sent
console.log(`Request: ${options.method} ${options.url}`);
},
],
beforeRetry: [
(error, retryCount) => {
// => Hook before retry
console.log(`Retry ${retryCount} after error:`, error.message);
},
],
afterResponse: [
(response) => {
// => Hook after response received
console.log(`Response: ${response.statusCode}`);
return response;
},
],
},
});
// Usage (automatic retry and timeout)
try {
const users = await api.get("users").json();
// => Automatically retries on failure
// => Automatically times out after 5 seconds
} catch (error) {
console.error("Request failed:", error);
}Production benefits:
- Built-in retry: Automatic exponential backoff
- Timeouts: Request, response, and send timeouts
- Hooks: Before/after request, retry, response
- Promise and stream API: Flexible response handling
- Caching: Built-in HTTP caching support
- TypeScript: Full TypeScript support
Trade-offs:
- External dependency: Got library (180KB)
- Different API: Not fetch-compatible (learning curve)
- Node.js only: Not browser-compatible
When to use Got:
- Node.js applications (server-side only)
- Advanced retry logic (custom backoff)
- HTTP caching required
- Stream processing (large files)
API Client Pattern Comparison
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
%% All colors are color-blind friendly and meet WCAG AA contrast standards
flowchart TD
A[Choose HTTP Client] --> B{Requirements?}
B -->|Built-in<br/>Simple GET/POST| C[fetch]
B -->|Interceptors<br/>Retry<br/>Browser + Node| D[axios]
B -->|Advanced retry<br/>Hooks<br/>Node.js only| E[got]
C --> C1[fetch Features]
D --> D1[axios Features]
E --> E1[got Features]
C1 --> C2[✓ Built-in Node 18+<br/>✓ Promise-based<br/>✓ AbortController<br/>✗ No retry<br/>✗ No interceptors]
D1 --> D2[✓ Interceptors<br/>✓ Auto JSON parse<br/>✓ Retry with plugin<br/>✓ Progress events<br/>✗ External dep 135KB]
E1 --> E2[✓ Built-in retry<br/>✓ Hooks<br/>✓ HTTP caching<br/>✓ Streams<br/>✗ External dep 180KB<br/>✗ Node.js only]
C2 --> F[Use Cases]
D2 --> F
E2 --> F
F --> F1[fetch: Simple apps<br/>Browser compatibility<br/>No dependencies]
F --> F2[axios: Production apps<br/>Complex APIs<br/>Interceptors needed]
F --> F3[got: Node.js services<br/>Advanced retry<br/>HTTP caching]
style A fill:#0173B2,stroke:#333,stroke-width:2px,color:#fff
style C fill:#029E73,stroke:#333,stroke-width:2px,color:#fff
style D fill:#DE8F05,stroke:#333,stroke-width:2px,color:#fff
style E fill:#029E73,stroke:#333,stroke-width:2px,color:#fff
Production Best Practices
API Client Abstraction
Create typed API client for specific service.
Pattern:
import axios, { AxiosInstance } from "axios";
import { z } from "zod";
// API response schemas
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const postSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
authorId: z.number(),
});
type User = z.infer<typeof userSchema>;
type Post = z.infer<typeof postSchema>;
class ApiClient {
// => Type-safe API client
private client: AxiosInstance;
constructor(baseURL: string, apiKey: string) {
this.client = axios.create({
baseURL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error) => {
console.error("API Error:", error.response?.data);
throw error;
},
);
}
async getUser(userId: number): Promise<User> {
// => Get user by ID
// => Returns validated User object
const response = await this.client.get(`/users/${userId}`);
return userSchema.parse(response.data);
// => Validate response against schema
}
async getUsers(limit: number = 10): Promise<User[]> {
// => Get list of users
const response = await this.client.get("/users", {
params: { limit },
});
return z.array(userSchema).parse(response.data);
// => Validate array of users
}
async createUser(user: Omit<User, "id">): Promise<User> {
// => Create new user
// => Omit<User, 'id'>: User without id field
const response = await this.client.post("/users", user);
return userSchema.parse(response.data);
}
async updateUser(userId: number, updates: Partial<User>): Promise<User> {
// => Update user
// => Partial<User>: All fields optional
const response = await this.client.patch(`/users/${userId}`, updates);
return userSchema.parse(response.data);
}
async deleteUser(userId: number): Promise<void> {
// => Delete user
await this.client.delete(`/users/${userId}`);
}
async getUserPosts(userId: number): Promise<Post[]> {
// => Get posts by user
const response = await this.client.get(`/users/${userId}/posts`);
return z.array(postSchema).parse(response.data);
}
}
// Usage
const api = new ApiClient("https://api.example.com", process.env.API_KEY!);
const user = await api.getUser(123);
// => Type-safe: user.name is string
console.log(user.name);
const newUser = await api.createUser({
name: "Alice",
email: "alice@example.com",
});
const posts = await api.getUserPosts(user.id);
// => Type-safe: posts[0].title is string
Rate Limiting and Backoff
Handle API rate limits gracefully.
Pattern:
class RateLimitedApiClient {
// => API client with rate limit handling
private client: AxiosInstance;
private requestQueue: Array<() => Promise<any>> = [];
private processing = false;
constructor(baseURL: string) {
this.client = axios.create({ baseURL });
// Intercept 429 responses
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (axios.isAxiosError(error) && error.response?.status === 429) {
// => Rate limited
const retryAfter = parseInt(error.response.headers["retry-after"] || "60");
// => Retry-After header (seconds)
console.log(`Rate limited, waiting ${retryAfter} seconds`);
await this.sleep(retryAfter * 1000);
// => Wait before retry
return this.client.request(error.config!);
// => Retry original request
}
throw error;
},
);
}
private sleep(ms: number): Promise<void> {
// => Helper: Wait for specified milliseconds
return new Promise((resolve) => setTimeout(resolve, ms));
}
async request<T>(config: any): Promise<T> {
// => Make rate-limited request
const response = await this.client.request(config);
return response.data;
}
}Request Caching
Cache API responses to reduce requests.
Pattern:
import NodeCache from "node-cache";
// => Install: npm install node-cache
class CachedApiClient {
// => API client with response caching
private client: AxiosInstance;
private cache: NodeCache;
constructor(baseURL: string) {
this.client = axios.create({ baseURL });
this.cache = new NodeCache({
stdTTL: 300,
// => Default TTL: 5 minutes
checkperiod: 60,
// => Check for expired keys every minute
});
}
async get<T>(url: string, options?: any): Promise<T> {
// => GET request with caching
const cacheKey = `${url}:${JSON.stringify(options?.params || {})}`;
// => Cache key includes URL and query params
const cached = this.cache.get<T>(cacheKey);
if (cached) {
// => Cache hit
console.log(`Cache hit: ${cacheKey}`);
return cached;
}
// Cache miss
console.log(`Cache miss: ${cacheKey}`);
const response = await this.client.get(url, options);
this.cache.set(cacheKey, response.data);
// => Store in cache
return response.data;
}
invalidateCache(pattern: string): void {
// => Invalidate cached entries by pattern
const keys = this.cache.keys();
keys.forEach((key) => {
if (key.includes(pattern)) {
this.cache.del(key);
// => Delete matching cache entry
}
});
}
}
// Usage
const api = new CachedApiClient("https://api.example.com");
const user1 = await api.get("/users/123");
// => Cache miss, makes API request
const user2 = await api.get("/users/123");
// => Cache hit, returns cached data
api.invalidateCache("users/123");
// => Invalidate cache for user 123
Trade-offs and When to Use Each
fetch
Use when:
- Node.js 18+ (built-in)
- Simple API calls (no retry, interceptors)
- Browser compatibility important (same API)
- Minimal dependencies preferred
Avoid when:
- Complex error handling (manual retry)
- Global auth headers (no interceptors)
- Old Node.js versions (<18)
axios
Use when:
- Production applications (robust error handling)
- Request/response interceptors needed
- Automatic retry required (with axios-retry)
- Browser and Node.js compatibility
Avoid when:
- Bundle size critical (135KB)
- Simple GET requests only (fetch sufficient)
got
Use when:
- Node.js server-side applications
- Built-in retry without plugins
- HTTP caching required
- Stream processing (large files)
Avoid when:
- Browser compatibility required (Node.js only)
- fetch-compatible API preferred
Common Pitfalls
Pitfall 1: Not Validating API Responses
Problem: Malformed API responses cause runtime errors.
Solution: Use Zod to validate responses.
// ❌ BAD: No validation
const user = await axios.get("/users/123").then((res) => res.data);
console.log(user.name.toUpperCase());
// => Runtime error if name is null or missing
// ✅ GOOD: Validate with Zod
const user = userSchema.parse(await axios.get("/users/123").then((res) => res.data));
console.log(user.name.toUpperCase());
// => Type-safe, validated
Pitfall 2: No Timeout Handling
Problem: Requests hang indefinitely.
Solution: Set timeout on all requests.
// ❌ BAD: No timeout
await axios.get("https://slow-api.com/data");
// => Hangs forever if server unresponsive
// ✅ GOOD: Timeout set
await axios.get("https://slow-api.com/data", { timeout: 5000 });
// => Aborts after 5 seconds
Pitfall 3: Exposing API Keys
Problem: API keys committed to git.
Solution: Use environment variables.
// ❌ BAD: Hardcoded API key
const api = axios.create({
headers: { Authorization: "Bearer sk_live_abc123" },
});
// ✅ GOOD: Environment variable
const api = axios.create({
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});Pitfall 4: Not Handling Rate Limits
Problem: API rate limits cause failures.
Solution: Implement retry with exponential backoff.
// ❌ BAD: No rate limit handling
await axios.get("/api/data");
// => Fails on 429 status
// ✅ GOOD: Retry on rate limit
axiosRetry(axios, {
retries: 3,
retryCondition: (error) => error.response?.status === 429,
});Summary
JSON and API integration enable modern applications to communicate with external services. Built-in JSON methods provide basic serialization, fetch offers Promise-based HTTP requests, axios adds interceptors and retry logic, and got provides advanced features for Node.js applications.
Progression path:
- Learn with JSON.parse/stringify: Understand JSON serialization fundamentals
- Request with fetch: Promise-based HTTP requests (Node.js 18+)
- Integrate with axios: Production API client with interceptors and retry
- Validate with Zod: Type-safe API responses
Production checklist:
- ✅ Response validation with Zod (type safety)
- ✅ Timeout on all requests (prevent hanging)
- ✅ Retry logic with exponential backoff (transient failures)
- ✅ Request/response interceptors (auth, logging)
- ✅ Error handling (network, HTTP, validation errors)
- ✅ API keys in environment variables (never hardcoded)
- ✅ Rate limit handling (429 status code)
- ✅ Response caching (reduce API calls)
- ✅ Typed API client class (encapsulate API logic)
Recommended approach: axios (interceptors + retry) + Zod (validation) for production applications. Use fetch for simple scripts or when built-in solution preferred.
Choose HTTP client based on requirements: fetch for simple calls, axios for production with interceptors, got for Node.js with advanced retry and caching.