TypeScript Fetch API
Introduction
The Fetch API is a modern interface for making HTTP requests in web applications. It provides a more powerful and flexible feature set than older techniques like XMLHttpRequest. When combined with TypeScript, you get the additional benefits of type safety, code completion, and better error handling.
In this guide, we'll explore how to use the Fetch API with TypeScript to make HTTP requests, handle responses, work with JSON data, and implement error handling in your web applications.
Understanding the Fetch API
The Fetch API provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. The primary function is fetch()
, which returns a Promise that resolves to the Response object representing the response to the request.
Basic Syntax
fetch(url, options)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Setting Up TypeScript for Fetch API
Before diving into examples, let's set up the proper TypeScript types for our Fetch operations:
// Define a type for our data
interface User {
id: number;
name: string;
email: string;
}
// Define a type for API response
interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
Making a Basic GET Request
Let's start with a simple GET request to retrieve data:
function fetchUsers(): Promise<User[]> {
return fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<User[]>;
});
}
// Usage
fetchUsers()
.then(users => {
console.log('Users:', users);
users.forEach(user => {
console.log(`User ${user.id}: ${user.name} (${user.email})`);
});
})
.catch(error => {
console.error('Failed to fetch users:', error);
});
Output
Users: [{ id: 1, name: "John Doe", email: "[email protected]" }, ...]
User 1: John Doe ([email protected])
User 2: Jane Smith ([email protected])
...
Using Async/Await with Fetch
TypeScript works wonderfully with the modern async/await syntax, making your fetch code much cleaner:
async function fetchUserById(id: number): Promise<User> {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const user = await response.json() as User;
return user;
} catch (error) {
console.error(`Failed to fetch user with ID ${id}:`, error);
throw error; // Re-throw to allow further handling
}
}
// Usage
async function displayUser(id: number): Promise<void> {
try {
const user = await fetchUserById(id);
console.log(`Found user: ${user.name} (${user.email})`);
} catch (error) {
console.error('Error displaying user:', error);
}
}
// Call the function
displayUser(1);
Output
Found user: John Doe ([email protected])
POST Requests with TypeScript
Creating a new resource typically requires a POST request:
async function createUser(userData: Omit<User, 'id'>): Promise<User> {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<User>;
}
// Usage
const newUser = {
name: 'Alice Johnson',
email: '[email protected]'
};
createUser(newUser)
.then(createdUser => {
console.log('User created successfully:', createdUser);
})
.catch(error => {
console.error('Failed to create user:', error);
});
Output
User created successfully: { id: 3, name: "Alice Johnson", email: "[email protected]" }
Creating a Generic Fetch Utility
Let's create a reusable, type-safe fetch utility that can be used throughout your application:
class HttpClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<T>;
}
async post<T, U>(endpoint: string, data: T): Promise<U> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<U>;
}
async put<T, U>(endpoint: string, data: T): Promise<U> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<U>;
}
async delete<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<T>;
}
}
// Usage
const apiClient = new HttpClient('https://api.example.com');
// GET request
const getUsers = async (): Promise<void> => {
try {
const users = await apiClient.get<User[]>('/users');
console.log('Users:', users);
} catch (error) {
console.error('Error fetching users:', error);
}
};
// POST request
const addUser = async (user: Omit<User, 'id'>): Promise<void> => {
try {
const newUser = await apiClient.post<Omit<User, 'id'>, User>('/users', user);
console.log('New user created:', newUser);
} catch (error) {
console.error('Error creating user:', error);
}
};
Advanced Features
Working with Request Headers
TypeScript helps ensure you're using the correct header names and values:
type HeadersInit = Headers | string[][] | Record<string, string>;
function fetchWithAuth(url: string, token: string): Promise<Response> {
const headers: HeadersInit = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
return fetch(url, {
headers
});
}
// Usage
const token = 'your-auth-token';
fetchWithAuth('https://api.example.com/protected-resource', token)
.then(response => response.json())
.then(data => console.log('Protected data:', data))
.catch(error => console.error('Auth error:', error));
Handling Different Response Types
The Fetch API can handle various response types, not just JSON:
async function fetchImage(url: string): Promise<Blob> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status}`);
}
return response.blob();
}
// Usage
fetchImage('https://example.com/image.jpg')
.then(imageBlob => {
const imageUrl = URL.createObjectURL(imageBlob);
const imgElement = document.createElement('img');
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
})
.catch(error => console.error('Image fetch error:', error));
Handling Timeouts
The Fetch API doesn't natively support timeouts, but we can implement it with TypeScript:
function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = 5000
): Promise<T> {
return Promise.race([
fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<T>;
}),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Request timed out after ${timeout}ms`)), timeout)
)
]);
}
// Usage
fetchWithTimeout<User[]>('https://api.example.com/users', {}, 3000)
.then(users => console.log('Users:', users))
.catch(error => console.error('Error or timeout:', error.message));
Handling Request Cancellation
With the AbortController API, you can cancel fetch requests:
function createCancellableFetch<T>(url: string, options: RequestInit = {}): {
promise: Promise<T>,
cancel: () => void
} {
const controller = new AbortController();
const { signal } = controller;
const promise = fetch(url, { ...options, signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<T>;
});
return {
promise,
cancel: () => controller.abort()
};
}
// Usage
const { promise, cancel } = createCancellableFetch<User[]>('https://api.example.com/users');
// To cancel the request after 2 seconds
setTimeout(() => {
console.log('Cancelling request...');
cancel();
}, 2000);
promise
.then(users => console.log('Users:', users))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.error('Error:', error);
}
});
Real-World Example: Building a Data Service
Let's create a complete data service for a blog application:
// Define our data models
interface Post {
id: number;
title: string;
body: string;
authorId: number;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
// Create a BlogService class
class BlogService {
private apiClient: HttpClient;
constructor(baseUrl: string) {
this.apiClient = new HttpClient(baseUrl);
}
// Post methods
async getPosts(): Promise<Post[]> {
return this.apiClient.get<Post[]>('/posts');
}
async getPostById(id: number): Promise<Post> {
return this.apiClient.get<Post>(`/posts/${id}`);
}
async createPost(post: Omit<Post, 'id'>): Promise<Post> {
return this.apiClient.post<Omit<Post, 'id'>, Post>('/posts', post);
}
async updatePost(id: number, post: Partial<Post>): Promise<Post> {
return this.apiClient.put<Partial<Post>, Post>(`/posts/${id}`, post);
}
async deletePost(id: number): Promise<void> {
return this.apiClient.delete<void>(`/posts/${id}`);
}
// Comment methods
async getCommentsByPostId(postId: number): Promise<Comment[]> {
return this.apiClient.get<Comment[]>(`/posts/${postId}/comments`);
}
async addComment(comment: Omit<Comment, 'id'>): Promise<Comment> {
return this.apiClient.post<Omit<Comment, 'id'>, Comment>('/comments', comment);
}
}
// Usage
const blogService = new BlogService('https://jsonplaceholder.typicode.com');
async function displayPostWithComments(postId: number): Promise<void> {
try {
const [post, comments] = await Promise.all([
blogService.getPostById(postId),
blogService.getCommentsByPostId(postId)
]);
console.log('Post:', post);
console.log(`Comments (${comments.length}):`);
comments.forEach(comment => {
console.log(`- ${comment.name} (${comment.email}): ${comment.body.substring(0, 50)}...`);
});
} catch (error) {
console.error('Error displaying post:', error);
}
}
// Call the function
displayPostWithComments(1);
Flow of a Fetch Request
Here's a diagram showing the flow of a typical fetch request:
Error Handling Best Practices
TypeScript can help create more robust error handling:
// Define custom error types
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ApiError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
// Enhanced fetch with better error handling
async function enhancedFetch<T>(url: string, options: RequestInit = {}): Promise<T> {
try {
const response = await fetch(url, options);
if (!response.ok) {
// Handle different status codes
if (response.status === 404) {
throw new ApiError('Resource not found', response.status);
} else if (response.status === 401) {
throw new ApiError('Unauthorized access', response.status);
} else {
throw new ApiError(`API error: ${response.statusText}`, response.status);
}
}
return await response.json() as T;
} catch (error) {
// Handle network errors
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new NetworkError('Network connectivity issue');
}
// Re-throw other errors
throw error;
}
}
// Usage with proper error handling
async function fetchUserWithErrorHandling(id: number): Promise<void> {
try {
const user = await enhancedFetch<User>(`https://api.example.com/users/${id}`);
console.log('User:', user);
} catch (error) {
if (error instanceof NetworkError) {
console.error('Network error occurred. Please check your connection.');
} else if (error instanceof ApiError) {
if (error.status === 404) {
console.error(`User with ID ${id} not found.`);
} else if (error.status === 401) {
console.error('Authentication required. Please log in again.');
} else {
console.error(`API error (${error.status}): ${error.message}`);
}
} else {
console.error('Unknown error:', error);
}
}
}
Summary
In this guide, we've explored how to use the Fetch API with TypeScript to make web requests. We've covered:
- Setting up TypeScript for making HTTP requests
- Making GET, POST, PUT, and DELETE requests
- Handling responses and different data types
- Creating reusable HTTP client utilities
- Working with request headers
- Implementing advanced features like timeouts and request cancellation
- Building a real-world data service
- Implementing robust error handling
By combining TypeScript's type safety with the Fetch API's modern approach to HTTP requests, you can build more reliable, maintainable, and robust web applications.
Additional Resources and Exercises
Practice Exercises
-
Basic Fetch Exercise: Create a function that fetches and displays a list of products from a fictional e-commerce API.
-
Error Handling Exercise: Enhance the product fetch function to handle different error types (network errors, 404 errors, server errors).
-
Advanced Exercise: Create a paginated data fetch utility that loads data in chunks when the user scrolls to the bottom of the page.
-
Authentication Exercise: Build a login service that stores and sends authentication tokens with each request.
Further Reading
- TypeScript Documentation
- MDN Fetch API Guide
- Best Practices for API Error Handling
- TypeScript Generics
Keep practicing and experimenting with TypeScript and the Fetch API to build increasingly complex and robust web applications!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)