Skip to main content

TypeScript API Consumption

Modern web applications rarely exist in isolation. They typically communicate with backend services through APIs to fetch data, submit information, authenticate users, and more. TypeScript provides powerful tools that make API consumption safer and more developer-friendly compared to plain JavaScript.

Introduction

API consumption is a fundamental skill for web developers. With TypeScript, you can add type safety to your API calls, making your code more robust and easier to maintain. This guide will walk you through the process of consuming APIs with TypeScript, from basic HTTP requests to building reusable API services.

Understanding API Basics

Before diving into TypeScript-specific aspects, let's quickly review what APIs are.

What is an API?

An API (Application Programming Interface) defines how different software components should interact with each other. In web development, we typically work with HTTP/REST APIs that expose endpoints we can call to perform operations.

Common HTTP Methods

  • GET: Retrieve data
  • POST: Create new data
  • PUT: Update existing data
  • DELETE: Remove data
  • PATCH: Partially update data

Making HTTP Requests in TypeScript

TypeScript provides several ways to make HTTP requests:

1. Using the Fetch API

The Fetch API is built into modern browsers and is available in Node.js with minimal polyfills.

typescript
// Define interface for the response
interface User {
id: number;
name: string;
email: string;
}

async function getUser(userId: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${userId}`);

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

return response.json() as Promise<User>;
}

// Usage
async function displayUserInfo() {
try {
const user = await getUser(123);
console.log(`User: ${user.name} (${user.email})`);
} catch (error) {
console.error('Failed to fetch user:', error);
}
}

2. Using Axios

Axios is a popular HTTP client library that works in both browser and Node.js environments. It provides a more consistent API and better defaults than the Fetch API.

First, you'll need to install Axios:

bash
npm install axios

Then you can use it in your TypeScript code:

typescript
import axios from 'axios';

interface User {
id: number;
name: string;
email: string;
}

async function getUser(userId: number): Promise<User> {
const response = await axios.get<User>(`https://api.example.com/users/${userId}`);
return response.data; // Axios automatically parses JSON and returns the data
}

// Usage
async function displayUserInfo() {
try {
const user = await getUser(123);
console.log(`User: ${user.name} (${user.email})`);
} catch (error) {
console.error('Failed to fetch user:', error);
}
}

Type Definitions for API Data

One of TypeScript's greatest strengths is its ability to define and enforce types for your data. This is especially valuable when working with APIs.

Creating Interfaces for API Responses

typescript
// Define interfaces based on your API's response structure
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest'; // Union type for restricted values
lastLogin?: Date; // Optional property
}

interface Post {
id: number;
userId: number;
title: string;
body: string;
tags: string[];
publishedAt: string; // ISO date string
}

// Using the interfaces
async function getUserPosts(userId: number): Promise<Post[]> {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

return response.json() as Promise<Post[]>;
}

Handling Nested API Data

APIs often return nested data structures. TypeScript interfaces can model these relationships:

typescript
interface Comment {
id: number;
text: string;
author: string;
createdAt: string;
}

interface Post {
id: number;
title: string;
content: string;
comments: Comment[];
author: {
id: number;
name: string;
avatarUrl: string;
};
}

Error Handling in API Calls

Proper error handling is crucial when working with APIs, as many things can go wrong in network communications.

Error Response Types

typescript
// Define error response structure
interface ApiError {
status: number;
message: string;
details?: string;
}

async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);

if (!response.ok) {
// Try to parse error as JSON
try {
const errorData = await response.json() as ApiError;
throw new Error(`API Error: ${errorData.message}`);
} catch (parseError) {
// If error parsing fails, fall back to status text
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
}

return response.json() as Promise<T>;
}

Using try/catch with Async/Await

typescript
async function getUserData(userId: number): Promise<User> {
try {
const user = await fetchData<User>(`https://api.example.com/users/${userId}`);
return user;
} catch (error) {
console.error('Error fetching user data:', error);
// You might want to show a user-friendly error message
throw new Error('Unable to load user data. Please try again later.');
}
}

Building Reusable API Services

For larger applications, it's a good practice to centralize your API calls in service classes.

Creating an API Service Class

typescript
// api.service.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

export class ApiService {
private api: AxiosInstance;

constructor(baseURL: string) {
this.api = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});

// Add request interceptor for authentication
this.api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
}

async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.api.get(url, config);
return response.data;
}

async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.api.post(url, data, config);
return response.data;
}

async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.api.put(url, data, config);
return response.data;
}

async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.api.delete(url, config);
return response.data;
}
}

Using the API Service

typescript
// user.service.ts
import { ApiService } from './api.service';

interface User {
id: number;
name: string;
email: string;
}

export class UserService {
private apiService: ApiService;

constructor(baseURL: string) {
this.apiService = new ApiService(baseURL);
}

async getUsers(): Promise<User[]> {
return this.apiService.get<User[]>('/users');
}

async getUserById(id: number): Promise<User> {
return this.apiService.get<User>(`/users/${id}`);
}

async createUser(userData: Omit<User, 'id'>): Promise<User> {
return this.apiService.post<User>('/users', userData);
}

async updateUser(id: number, userData: Partial<User>): Promise<User> {
return this.apiService.put<User>(`/users/${id}`, userData);
}

async deleteUser(id: number): Promise<void> {
return this.apiService.delete<void>(`/users/${id}`);
}
}

// Usage example
const userService = new UserService('https://api.example.com');

async function manageUsers() {
// Get all users
const users = await userService.getUsers();
console.log('All users:', users);

// Create a new user
const newUser = await userService.createUser({
name: 'John Doe',
email: '[email protected]'
});
console.log('Created user:', newUser);

// Update user
const updatedUser = await userService.updateUser(newUser.id, {
name: 'John Smith'
});
console.log('Updated user:', updatedUser);
}

Working with Authentication

Most APIs require authentication. Here's how to handle it in TypeScript:

JWT Authentication

typescript
// auth.service.ts
import { ApiService } from './api.service';

interface LoginCredentials {
email: string;
password: string;
}

interface LoginResponse {
token: string;
user: {
id: number;
name: string;
email: string;
};
}

export class AuthService {
private apiService: ApiService;
private tokenKey = 'auth_token';

constructor(baseURL: string) {
this.apiService = new ApiService(baseURL);
}

async login(credentials: LoginCredentials): Promise<boolean> {
try {
const response = await this.apiService.post<LoginResponse>('/login', credentials);
this.saveToken(response.token);
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
}
}

logout(): void {
localStorage.removeItem(this.tokenKey);
}

isAuthenticated(): boolean {
return !!this.getToken();
}

getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}

private saveToken(token: string): void {
localStorage.setItem(this.tokenKey, token);
}
}

Real-world Example: Building a Weather App

Let's tie everything together with a real-world example - a simple weather app that fetches data from a weather API.

typescript
// weather.types.ts
export interface WeatherResponse {
location: {
name: string;
country: string;
};
current: {
temp_c: number;
temp_f: number;
condition: {
text: string;
icon: string;
};
wind_kph: number;
humidity: number;
feelslike_c: number;
};
forecast: {
forecastday: ForecastDay[];
};
}

export interface ForecastDay {
date: string;
day: {
maxtemp_c: number;
mintemp_c: number;
condition: {
text: string;
icon: string;
};
};
}

// weather.service.ts
import { ApiService } from './api.service';
import { WeatherResponse } from './weather.types';

export class WeatherService {
private apiService: ApiService;
private apiKey: string;

constructor(baseURL: string, apiKey: string) {
this.apiService = new ApiService(baseURL);
this.apiKey = apiKey;
}

async getWeatherForCity(city: string): Promise<WeatherResponse> {
return this.apiService.get<WeatherResponse>('/forecast.json', {
params: {
key: this.apiKey,
q: city,
days: 3,
aqi: 'no',
alerts: 'no'
}
});
}
}

// weather-app.ts
import { WeatherService } from './weather.service';

class WeatherApp {
private weatherService: WeatherService;
private cityInput: HTMLInputElement;
private weatherDisplay: HTMLDivElement;

constructor() {
this.weatherService = new WeatherService(
'https://api.weatherapi.com/v1',
'YOUR_API_KEY'
);

this.cityInput = document.getElementById('city-input') as HTMLInputElement;
this.weatherDisplay = document.getElementById('weather-display') as HTMLDivElement;

this.setupEventListeners();
}

private setupEventListeners(): void {
const searchButton = document.getElementById('search-button');
if (searchButton) {
searchButton.addEventListener('click', () => this.fetchWeather());
}

this.cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.fetchWeather();
}
});
}

private async fetchWeather(): Promise<void> {
const city = this.cityInput.value.trim();
if (!city) return;

try {
const weatherData = await this.weatherService.getWeatherForCity(city);
this.displayWeather(weatherData);
} catch (error) {
console.error('Failed to fetch weather:', error);
this.weatherDisplay.innerHTML = `
<div class="error">
Failed to fetch weather data. Please try again.
</div>
`;
}
}

private displayWeather(data: WeatherResponse): void {
const current = data.current;
const location = data.location;

this.weatherDisplay.innerHTML = `
<div class="weather-card">
<h2>${location.name}, ${location.country}</h2>
<div class="current-weather">
<img src="${current.condition.icon}" alt="${current.condition.text}" />
<div class="temp">${current.temp_c}°C</div>
<div class="condition">${current.condition.text}</div>
</div>
<div class="details">
<div>Feels like: ${current.feelslike_c}°C</div>
<div>Wind: ${current.wind_kph} kph</div>
<div>Humidity: ${current.humidity}%</div>
</div>
<div class="forecast">
<h3>3-Day Forecast</h3>
<div class="forecast-days">
${data.forecast.forecastday.map(day => `
<div class="forecast-day">
<div>${new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' })}</div>
<img src="${day.day.condition.icon}" alt="${day.day.condition.text}" />
<div>${day.day.mintemp_c}° - ${day.day.maxtemp_c}°</div>
</div>
`).join('')}
</div>
</div>
</div>
`;
}
}

// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
new WeatherApp();
});

Best Practices for API Consumption in TypeScript

  1. Define Type Interfaces: Always define interfaces for API request and response data.

  2. Centralize API Logic: Create reusable services for API calls instead of scattering fetch/axios calls throughout your application.

  3. Proper Error Handling: Always handle potential errors in your API calls, providing fallbacks and user-friendly messages.

  4. Environment Variables: Store API keys and base URLs in environment variables, not hard-coded in your source.

  5. Request Cancelation: Implement request cancelation for long-running requests, especially in UI components that might unmount before the request completes.

  6. Caching: Consider implementing caching for frequently accessed and rarely changing data.

  7. Rate Limiting: Be mindful of API rate limits and implement throttling if necessary.

Summary

In this guide, we've covered the essentials of consuming APIs with TypeScript:

  • Making HTTP requests using Fetch API and Axios
  • Creating type definitions for API data
  • Handling errors properly
  • Building reusable API services
  • Implementing authentication
  • Working with a real-world example

TypeScript significantly improves the developer experience when working with APIs by providing type safety, better IntelliSense, and clearer error messages. This leads to more robust applications with fewer runtime errors.

Further Resources

Exercises

  1. Create a TypeScript interface for a TODO item from the JSONPlaceholder API and implement a service to fetch and manipulate TODOs.

  2. Add error handling to the Weather App example that displays different error messages based on the API error code.

  3. Extend the ApiService class to include request caching using a simple in-memory cache.

  4. Create a service that uses TypeScript generics to fetch data from any endpoint with type safety.

  5. Implement token refresh logic in the AuthService to automatically refresh expired JWT tokens.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)