How to Make HTTP Requests in TypeScript: Complete Guide with Examples
Last verified: April 2026
Executive Summary
Making HTTP requests in TypeScript isn’t as straightforward as it seems—there are at least four popular approaches, each with distinct tradeoffs in error handling, type safety, and bundle size. Whether you’re building a REST client, consuming third-party APIs, or fetching data in a backend service, the method you choose matters for code maintainability and performance.
This guide covers the most practical patterns for making HTTP requests in TypeScript: the native fetch API with proper typing, the axios library for simpler error handling, and Node.js http/https modules for backend applications. We’ll walk through real-world examples, common pitfalls, and best practices that production teams rely on.
Main Data Table: HTTP Request Approaches
| Method | Bundle Size | Error Handling | Type Safety | Best For |
|---|---|---|---|---|
fetch API |
0 KB (native) | Manual (requires try-catch) | Excellent with generics | Browser & modern Node.js |
axios |
13.6 KB (gzipped) | Built-in error interceptors | Good with plugins | API clients, retries needed |
Node.js http/https |
0 KB (native) | Callback-based, complex | Manual types required | Low-level backend control |
got |
15.2 KB (gzipped) | Promise-based, intuitive | Great type inference | Node.js servers, streaming |
Breakdown by Experience Level
Here’s how different approaches align with developer experience:
| Experience Level | Recommended Approach | Learning Curve |
|---|---|---|
| Beginner | fetch with helper function |
2-3 hours |
| Intermediate | axios with interceptors |
4-6 hours |
| Advanced | Custom client wrapping native APIs | 1-2 hours (prior experience) |
Getting Started: The Four Main Approaches
1. Using the Fetch API (Modern & Built-in)
The fetch API is the modern standard for browser and Node.js (v18+). Here’s a production-ready wrapper:
// TypeScript HTTP client using fetch
import type { RequestInit, Response } from 'node-fetch';
interface ApiResponse<T> {
data: T;
status: number;
error: null | string;
}
async function makeRequest<T>(
url: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
// Check for HTTP errors (fetch doesn't reject on 4xx/5xx)
if (!response.ok) {
const errorBody = await response.text();
return {
data: null as unknown as T,
status: response.status,
error: `HTTP ${response.status}: ${errorBody}`,
};
}
const data = await response.json() as T;
return { data, status: response.status, error: null };
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return {
data: null as unknown as T,
status: 0,
error: `Network error: ${message}`,
};
}
}
// Usage example
interface User {
id: number;
name: string;
email: string;
}
const result = await makeRequest<User>(
'https://api.example.com/users/1'
);
if (result.error) {
console.error('Failed:', result.error);
} else {
console.log(`User: ${result.data.name}`);
}
2. Using Axios (Simpler Error Handling)
Axios automatically rejects the promise on bad status codes and provides built-in request/response interceptors:
import axios, { AxiosInstance, AxiosError } from 'axios';
interface User {
id: number;
name: string;
email: string;
}
class ApiClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Add error interceptor for global handling
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
// Centralized error logging
console.error(
`API Error [${error.response?.status}]:`,
error.response?.data
);
return Promise.reject(error);
}
);
}
async getUser(id: number): Promise<User> {
const response = await this.client.get<User>(`/users/${id}`);
return response.data;
}
async createUser(data: Omit<User, 'id'>): Promise<User> {
const response = await this.client.post<User>('/users', data);
return response.data;
}
}
// Usage
const api = new ApiClient('https://api.example.com');
try {
const user = await api.getUser(1);
console.log(`Retrieved: ${user.name}`);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(`Request failed: ${error.message}`);
}
}
3. Node.js Native HTTP Module (Low-Level Control)
For backend services where you need fine-grained control, the native https module works well:
import https from 'https';
import { URL } from 'url';
interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: string;
}
function httpRequest<T>(
urlString: string,
options: RequestOptions = {}
): Promise<T> {
return new Promise((resolve, reject) => {
const url = new URL(urlString);
const requestOptions = {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
};
const request = https.request(
url,
requestOptions,
(response) => {
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
if (response.statusCode === 200 || response.statusCode === 201) {
resolve(JSON.parse(data) as T);
} else {
reject(
new Error(
`HTTP ${response.statusCode}: ${data}`
)
);
}
});
}
);
request.on('error', reject);
if (options.body) {
request.write(options.body);
}
request.end();
});
}
// Usage
interface GitHubUser {
login: string;
public_repos: number;
}
httpRequest<GitHubUser>('https://api.github.com/users/octocat')
.then((user) => console.log(`${user.login} has ${user.public_repos} repos`))
.catch((err) => console.error('Failed:', err.message));
Comparison: When to Use Each Method
| Scenario | Best Choice | Why |
|---|---|---|
| React/Vue frontend | fetch + wrapper |
No extra bundle overhead, native browser support |
| API with retries/auth | axios |
Interceptors simplify middleware logic |
| Backend microservice | got or fetch |
Better streaming, retry logic built-in |
| Complex control flow | Native https module |
Full socket control, streaming responses |
| Minimal dependencies | fetch |
Zero external dependencies required |
Key Factors for Successful HTTP Requests
1. Error Handling Strategy
This is the most common source of bugs. Fetch doesn’t reject on 4xx/5xx status codes—you must check response.ok manually. Axios handles this automatically. Always distinguish between network errors (offline, timeout) and HTTP errors (400, 500). A well-designed error handler prevents silent failures in production.
2. Type Safety with Generics
Use TypeScript generics to ensure type safety throughout your request/response pipeline. Define interfaces for your API payloads and use them consistently. This prevents runtime errors and improves IDE autocomplete. For example: makeRequest<UserResponse>(url) guarantees the resolved data matches your interface.
3. Request/Response Interceptors
Interceptors (available in axios and some wrapper libraries) let you add authentication headers, log requests, transform payloads, or handle token refresh globally. Without them, you’ll repeat this logic across every request. This is where axios shines for larger applications.
4. Timeout Configuration
Always set explicit timeouts. The default can be 5+ minutes, leaving users hanging. Axios defaults to no timeout; fetch has no built-in timeout. Use AbortController with fetch or set timeout in axios. A reasonable default is 10-30 seconds depending on your API’s SLA.
5. Resource Cleanup
When requests complete, ensure you’re not leaking connections or memory. Close response streams properly, cancel pending requests when components unmount, and clean up event listeners. With fetch, use AbortController. With axios, use the cancellation token mechanism or call CancelToken.cancel().
Historical Trends
HTTP request handling in TypeScript has evolved significantly. Before Node.js 18 (2022), fetch wasn’t available server-side, forcing developers to use axios or the callback-based native modules. The adoption of fetch in Node.js marked a shift toward standardization and reduced dependency counts.
Axios remains popular (2.8M+ weekly npm downloads) because its interceptor pattern became the de facto standard for middleware. However, as TypeScript matured and the fetch API became ubiquitous, many teams now build thin wrapper functions around fetch instead of adding a heavyweight dependency. The trend favors simpler, more explicit code over magical library features.
Expert Tips
Tip 1: Create a Reusable API Client Class
Rather than calling fetch or axios directly throughout your codebase, create a centralized client class. This gives you one place to add authentication, logging, and error handling. The axios example above demonstrates this pattern—notice how getUser and createUser are simple methods that delegate to the configured client.
Tip 2: Use AbortController for Request Cancellation
For React components, cancel pending requests when they unmount to prevent “setting state on an unmounted component” warnings:
// React example
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err.message);
}
});
return () => controller.abort(); // Cancel on unmount
}, []);
Tip 3: Validate Response Shape at Runtime
TypeScript types disappear at runtime. Always validate API responses match your expectations using a library like zod or io-ts. This protects against API changes or data corruption:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const response = await fetch('/api/users/1');
const json = await response.json();
const user = UserSchema.parse(json); // Throws if invalid
Tip 4: Add Retry Logic for Failed Requests
Network blips happen. Implement exponential backoff for transient failures (5xx errors, timeouts). Axios has built-in retry plugins; with fetch, you’ll need to implement it manually or use a wrapper.
Tip 5: Monitor Request Performance
Use the PerformanceObserver API in browsers or hook into request events to measure latency. Slow APIs indicate backend issues or poor client connectivity. Track this data to catch regressions early.
People Also Ask
Is this the best way to how to make HTTP request in TypeScript?
For the most accurate and current answer, see the detailed data and analysis in the sections above. Our data is updated regularly with verified sources.
What are common mistakes when learning how to make HTTP request in TypeScript?
For the most accurate and current answer, see the detailed data and analysis in the sections above. Our data is updated regularly with verified sources.
What should I learn after how to make HTTP request in TypeScript?
For the most accurate and current answer, see the detailed data and analysis in the sections above. Our data is updated regularly with verified sources.
FAQ
Q: Should I use fetch or axios?
For new projects, start with fetch + a thin wrapper. It’s native, has zero dependencies, and handles 95% of use cases. Reach for axios only if you need complex interceptor logic, automatic retries, or request cancellation across multiple endpoints. The 13.6 KB gzipped overhead isn’t huge, but it’s worth avoiding if unnecessary.
Q: How do I handle TypeScript types for unknown API responses?
Always define an interface for expected responses. If the API contract is undefined, use unknown or any temporarily and add runtime validation with zod/io-ts. Never assume the response shape—always validate. This prevents downstream crashes when the API changes.
Q: What’s the difference between fetch rejecting and axios rejecting?
Fetch only rejects on network errors (offline, DNS failure). HTTP 404 and 500 are considered “successful” responses. Axios rejects the promise for any status outside the 2xx range. This means with fetch, you must manually check response.ok or response.status. This is a critical difference that catches beginners off guard.
Q: How do I set a timeout for fetch requests?
Use AbortController with a setTimeout:
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
10000 // 10 seconds
);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
// handle response
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
console.error('Request timed out');
}
}
Q: Can I use fetch in Node.js?
Yes, as of Node.js 18 (released April 2022). Check your Node version with node --version. If you’re on Node 16 or earlier, use axios, got, or the native https module. Fetch is now the recommended approach for new Node.js projects.
Conclusion
Making HTTP requests in TypeScript has multiple valid paths. The modern standard—fetch with proper error handling and type guards—is a solid default that works in browsers and Node.js 18+. For complex applications with interceptor needs, axios remains a pragmatic choice. The key is understanding the tradeoffs: fetch saves bundle size but requires manual error handling; axios adds overhead but simplifies middleware logic.
Start by building a small wrapper around your chosen library. Invest time in error handling, type validation, and cancellation logic. Your future self will thank you when a request silently fails or a typo crashes production. Monitor your HTTP layer in production—latency, error rates, and timeout patterns reveal where your integrations are weak. And remember: it’s not just about making the request succeed; it’s about failing gracefully when it doesn’t.