Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
LoginSign Up
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Apis Json
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Working with APIs and JSON

Typing API responses, fetch requests, and JSON data

Modern web applications rely heavily on APIs and JSON data. TypeScript provides powerful tools for typing API responses, validating data at runtime, and building type-safe API clients. Let's master API integration with TypeScript!

Typing API Responses

Define interfaces for your API response structures to get type safety and autocomplete.

api-types.ts
// Basic API response types
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: string;  // ISO date string from API
}

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  stock: number;
  category: string;
}

interface Order {
  id: number;
  userId: number;
  products: {
    productId: number;
    quantity: number;
    price: number;
  }[];
  total: number;
  status: 'pending' | 'processing' | 'shipped' | 'delivered';
  createdAt: string;
}

// Generic API wrapper types
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

interface ApiError {
  success: false;
  error: string;
  statusCode: number;
  details?: any;
}

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    pageSize: number;
    total: number;
    totalPages: number;
  };
}

// Basic fetch with typing
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  const user: User = await response.json();
  return user;
}

// Using generic wrapper
async function fetchUserWrapped(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const result: ApiResponse<User> = await response.json();
  
  if (!result.success) {
    throw new Error(result.message || 'Failed to fetch user');
  }
  
  return result.data;
}

// Paginated results
async function fetchProducts(page: number = 1): Promise<PaginatedResponse<Product>> {
  const response = await fetch(`/api/products?page=${page}&pageSize=20`);
  const data: PaginatedResponse<Product> = await response.json();
  return data;
}

// Usage
async function loadUserData() {
  try {
    const user = await fetchUser(1);
    console.log(`User: ${user.name}`);
    console.log(`Role: ${user.role}`);
    console.log(`Email: ${user.email}`);
    
    const products = await fetchProducts(1);
    console.log(`Loaded ${products.data.length} products`);
    console.log(`Total pages: ${products.pagination.totalPages}`);
  } catch (error) {
    console.error('Error:', (error as Error).message);
  }
}

Building a REST API Client

api-client.ts
// HTTP methods type
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

// Request configuration
interface RequestConfig {
  method?: HttpMethod;
  headers?: Record<string, string>;
  body?: any;
  params?: Record<string, string | number>;
  timeout?: number;
}

// API client class
class ApiClient {
  private baseUrl: string;
  private defaultHeaders: Record<string, string>;
  
  constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
    this.baseUrl = baseUrl;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...defaultHeaders
    };
  }
  
  // Set authentication token
  setAuthToken(token: string): void {
    this.defaultHeaders['Authorization'] = `Bearer ${token}`;
  }
  
  // Build URL with query parameters
  private buildUrl(endpoint: string, params?: Record<string, string | number>): string {
    const url = new URL(endpoint, this.baseUrl);
    
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        url.searchParams.append(key, String(value));
      });
    }
    
    return url.toString();
  }
  
  // Core request method
  private async request<T>(
    endpoint: string,
    config: RequestConfig = {}
  ): Promise<T> {
    const {
      method = 'GET',
      headers = {},
      body,
      params,
      timeout = 30000
    } = config;
    
    const url = this.buildUrl(endpoint, params);
    
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
      const response = await fetch(url, {
        method,
        headers: { ...this.defaultHeaders, ...headers },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal
      });
      
      clearTimeout(timeoutId);
      
      if (!response.ok) {
        const error: ApiError = await response.json();
        throw new Error(error.error || `HTTP error! status: ${response.status}`);
      }
      
      const data: T = await response.json();
      return data;
    } catch (error) {
      clearTimeout(timeoutId);
      
      if (error instanceof Error && error.name === 'AbortError') {
        throw new Error(`Request timeout after ${timeout}ms`);
      }
      
      throw error;
    }
  }
  
  // Convenience methods
  async get<T>(endpoint: string, params?: Record<string, string | number>): Promise<T> {
    return this.request<T>(endpoint, { params });
  }
  
  async post<T>(endpoint: string, body: any): Promise<T> {
    return this.request<T>(endpoint, { method: 'POST', body });
  }
  
  async put<T>(endpoint: string, body: any): Promise<T> {
    return this.request<T>(endpoint, { method: 'PUT', body });
  }
  
  async patch<T>(endpoint: string, body: any): Promise<T> {
    return this.request<T>(endpoint, { method: 'PATCH', body });
  }
  
  async delete<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'DELETE' });
  }
}

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

// Set token after login
api.setAuthToken('your-auth-token');

// GET request
const user = await api.get<User>('/users/1');
console.log(user.name);

// GET with query params
const products = await api.get<PaginatedResponse<Product>>('/products', {
  page: 1,
  category: 'electronics'
});

// POST request
interface CreateUserDto {
  name: string;
  email: string;
  password: string;
}

const newUser = await api.post<User>('/users', {
  name: 'Yusuf Ibrahim',
  email: 'yusuf@example.com',
  password: 'secure123'
});

// PUT request
const updatedUser = await api.put<User>(`/users/${user.id}`, {
  name: 'Yusuf A. Ibrahim'
});

// PATCH request
const patchedUser = await api.patch<User>(`/users/${user.id}`, {
  email: 'newemail@example.com'
});

// DELETE request
await api.delete(`/users/${user.id}`);

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

interface Product {
  id: number;
  name: string;
  price: number;
}

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    total: number;
  };
}

interface ApiError {
  error: string;
}

Runtime Data Validation

validation.ts
// Manual type guards
function isUser(obj: any): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    ['admin', 'user', 'guest'].includes(obj.role)
  );
}

function isProduct(obj: any): obj is Product {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.price === 'number' &&
    typeof obj.stock === 'number'
  );
}

// Validated fetch function
async function fetchValidatedUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();
  
  if (!isUser(data)) {
    throw new Error('Invalid user data structure');
  }
  
  return data;
}

// Array validation
function isUserArray(obj: any): obj is User[] {
  return Array.isArray(obj) && obj.every(isUser);
}

async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users');
  const data: unknown = await response.json();
  
  if (!isUserArray(data)) {
    throw new Error('Invalid users data structure');
  }
  
  return data;
}

// Generic validator
function createValidator<T>(
  checker: (obj: any) => obj is T
): (data: unknown) => T {
  return (data: unknown): T => {
    if (!checker(data)) {
      throw new Error('Validation failed');
    }
    return data;
  };
}

// Usage
const validateUser = createValidator(isUser);
const validateProduct = createValidator(isProduct);

async function loadData() {
  const response = await fetch('/api/user/1');
  const data = await response.json();
  
  try {
    const user = validateUser(data);
    console.log('Valid user:', user);
  } catch (error) {
    console.error('Invalid user data');
  }
}

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
}

💡 Use Runtime Validation Libraries

For production apps, consider using libraries like zod, yup, or io-ts for robust runtime validation. They provide better error messages and are easier to maintain than manual type guards.

Using Axios with TypeScript

axios-client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';

// Axios client with TypeScript
class AxiosApiClient {
  private client: AxiosInstance;
  
  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    this.setupInterceptors();
  }
  
  private setupInterceptors(): void {
    // Request interceptor
    this.client.interceptors.request.use(
      (config) => {
        // Add auth token if available
        const token = localStorage.getItem('token');
        if (token && config.headers) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
    
    // Response interceptor
    this.client.interceptors.response.use(
      (response) => response,
      (error: AxiosError<ApiError>) => {
        if (error.response) {
          // Server responded with error
          const apiError = error.response.data;
          console.error('API Error:', apiError.error);
          
          if (error.response.status === 401) {
            // Handle unauthorized
            console.log('Unauthorized - redirecting to login');
          }
        } else if (error.request) {
          // Request made but no response
          console.error('Network Error:', error.message);
        } else {
          // Something else happened
          console.error('Error:', error.message);
        }
        
        return Promise.reject(error);
      }
    );
  }
  
  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.get<T>(url, config);
    return response.data;
  }
  
  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.post<T>(url, data, config);
    return response.data;
  }
  
  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.put<T>(url, data, config);
    return response.data;
  }
  
  async patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.patch<T>(url, data, config);
    return response.data;
  }
  
  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.delete<T>(url, config);
    return response.data;
  }
}

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

// GET request
const user = await api.get<User>('/users/1');
console.log(user.name);

// POST request
const newUser = await api.post<User>('/users', {
  name: 'Yusuf Ibrahim',
  email: 'yusuf@example.com'
});

// Error handling
try {
  const user = await api.get<User>('/users/999');
} catch (error) {
  if (axios.isAxiosError(error)) {
    console.error('Status:', error.response?.status);
    console.error('Data:', error.response?.data);
  }
}

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

interface ApiError {
  error: string;
  statusCode: number;
}

Practical Examples

Example 1: Complete CRUD Operations

product-service.ts
interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  stock: number;
  category: string;
  createdAt: string;
  updatedAt: string;
}

interface CreateProductDto {
  name: string;
  description: string;
  price: number;
  stock: number;
  category: string;
}

interface UpdateProductDto {
  name?: string;
  description?: string;
  price?: number;
  stock?: number;
  category?: string;
}

class ProductService {
  private api: ApiClient;
  
  constructor(apiClient: ApiClient) {
    this.api = apiClient;
  }
  
  // Get all products
  async getAll(params?: {
    page?: number;
    pageSize?: number;
    category?: string;
    search?: string;
  }): Promise<PaginatedResponse<Product>> {
    return this.api.get<PaginatedResponse<Product>>('/products', params);
  }
  
  // Get single product
  async getById(id: number): Promise<Product> {
    return this.api.get<Product>(`/products/${id}`);
  }
  
  // Create product
  async create(data: CreateProductDto): Promise<Product> {
    return this.api.post<Product>('/products', data);
  }
  
  // Update product
  async update(id: number, data: UpdateProductDto): Promise<Product> {
    return this.api.patch<Product>(`/products/${id}`, data);
  }
  
  // Delete product
  async delete(id: number): Promise<void> {
    return this.api.delete(`/products/${id}`);
  }
  
  // Search products
  async search(query: string): Promise<Product[]> {
    const response = await this.api.get<PaginatedResponse<Product>>('/products', {
      search: query,
      pageSize: 50
    });
    return response.data;
  }
  
  // Get by category
  async getByCategory(category: string): Promise<Product[]> {
    const response = await this.api.get<PaginatedResponse<Product>>('/products', {
      category
    });
    return response.data;
  }
  
  // Update stock
  async updateStock(id: number, quantity: number): Promise<Product> {
    return this.api.patch<Product>(`/products/${id}`, { stock: quantity });
  }
}

// Usage
const api = new ApiClient('https://api.example.com');
const productService = new ProductService(api);

async function manageProducts() {
  try {
    // Get all products
    const { data: products, pagination } = await productService.getAll({
      page: 1,
      pageSize: 20
    });
    
    console.log(`Loaded ${products.length} products`);
    console.log(`Total: ${pagination.total}`);
    
    // Create new product
    const newProduct = await productService.create({
      name: 'Laptop',
      description: 'High-performance laptop',
      price: 450000,
      stock: 10,
      category: 'electronics'
    });
    
    console.log(`Created product: ${newProduct.name}`);
    
    // Update product
    const updated = await productService.update(newProduct.id, {
      price: 425000,
      stock: 15
    });
    
    console.log(`Updated price: ₦${updated.price}`);
    
    // Search products
    const searchResults = await productService.search('laptop');
    console.log(`Found ${searchResults.length} matching products`);
    
    // Get by category
    const electronics = await productService.getByCategory('electronics');
    console.log(`Electronics: ${electronics.length} items`);
    
    // Delete product
    await productService.delete(newProduct.id);
    console.log('Product deleted');
    
  } catch (error) {
    console.error('Error:', (error as Error).message);
  }
}

manageProducts();

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    pageSize: number;
    total: number;
    totalPages: number;
  };
}

class ApiClient {
  async get<T>(endpoint: string, params?: any): Promise<T> {
    return {} as T;
  }
  async post<T>(endpoint: string, body: any): Promise<T> {
    return {} as T;
  }
  async patch<T>(endpoint: string, body: any): Promise<T> {
    return {} as T;
  }
  async delete<T>(endpoint: string): Promise<T> {
    return {} as T;
  }
}

Example 2: Authentication Service

auth-service.ts
interface LoginCredentials {
  email: string;
  password: string;
}

interface RegisterData {
  name: string;
  email: string;
  password: string;
}

interface AuthResponse {
  user: User;
  token: string;
  refreshToken: string;
}

interface RefreshTokenResponse {
  token: string;
  refreshToken: string;
}

class AuthService {
  private api: ApiClient;
  private tokenKey = 'auth_token';
  private refreshTokenKey = 'refresh_token';
  
  constructor(apiClient: ApiClient) {
    this.api = apiClient;
  }
  
  async login(credentials: LoginCredentials): Promise<AuthResponse> {
    const response = await this.api.post<AuthResponse>('/auth/login', credentials);
    
    // Store tokens
    this.setTokens(response.token, response.refreshToken);
    
    // Set token in API client
    this.api.setAuthToken(response.token);
    
    return response;
  }
  
  async register(data: RegisterData): Promise<AuthResponse> {
    const response = await this.api.post<AuthResponse>('/auth/register', data);
    
    this.setTokens(response.token, response.refreshToken);
    this.api.setAuthToken(response.token);
    
    return response;
  }
  
  async logout(): Promise<void> {
    try {
      await this.api.post('/auth/logout', {});
    } finally {
      this.clearTokens();
    }
  }
  
  async refreshToken(): Promise<string> {
    const refreshToken = this.getRefreshToken();
    
    if (!refreshToken) {
      throw new Error('No refresh token available');
    }
    
    const response = await this.api.post<RefreshTokenResponse>('/auth/refresh', {
      refreshToken
    });
    
    this.setTokens(response.token, response.refreshToken);
    this.api.setAuthToken(response.token);
    
    return response.token;
  }
  
  async getCurrentUser(): Promise<User> {
    return this.api.get<User>('/auth/me');
  }
  
  async updatePassword(currentPassword: string, newPassword: string): Promise<void> {
    await this.api.post('/auth/password', {
      currentPassword,
      newPassword
    });
  }
  
  async resetPassword(email: string): Promise<void> {
    await this.api.post('/auth/reset-password', { email });
  }
  
  private setTokens(token: string, refreshToken: string): void {
    localStorage.setItem(this.tokenKey, token);
    localStorage.setItem(this.refreshTokenKey, refreshToken);
  }
  
  private getRefreshToken(): string | null {
    return localStorage.getItem(this.refreshTokenKey);
  }
  
  private clearTokens(): void {
    localStorage.removeItem(this.tokenKey);
    localStorage.removeItem(this.refreshTokenKey);
  }
  
  isAuthenticated(): boolean {
    return !!localStorage.getItem(this.tokenKey);
  }
}

// Usage
const api = new ApiClient('https://api.example.com');
const authService = new AuthService(api);

async function handleAuth() {
  try {
    // Login
    const { user, token } = await authService.login({
      email: 'yusuf@example.com',
      password: 'secure123'
    });
    
    console.log(`Logged in as: ${user.name}`);
    console.log(`Token: ${token}`);
    
    // Get current user
    const currentUser = await authService.getCurrentUser();
    console.log(`Current user: ${currentUser.name}`);
    
    // Check authentication
    if (authService.isAuthenticated()) {
      console.log('User is authenticated');
    }
    
    // Logout
    await authService.logout();
    console.log('Logged out successfully');
    
  } catch (error) {
    console.error('Auth error:', (error as Error).message);
  }
}

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

class ApiClient {
  setAuthToken(token: string): void {}
  async get<T>(endpoint: string): Promise<T> { return {} as T; }
  async post<T>(endpoint: string, body: any): Promise<T> { return {} as T; }
}

Example 3: Real-time Updates with Server-Sent Events

sse-client.ts
interface ServerEvent<T> {
  type: string;
  data: T;
  timestamp: string;
}

interface NotificationEvent {
  id: number;
  message: string;
  type: 'info' | 'warning' | 'error';
  read: boolean;
}

class SSEClient<T> {
  private eventSource: EventSource | null = null;
  private listeners = new Map<string, Set<(data: T) => void>>();
  
  constructor(private url: string) {}
  
  connect(): void {
    if (this.eventSource) {
      return;  // Already connected
    }
    
    this.eventSource = new EventSource(this.url);
    
    this.eventSource.onopen = () => {
      console.log('SSE connection established');
    };
    
    this.eventSource.onerror = (error) => {
      console.error('SSE error:', error);
      this.disconnect();
    };
    
    this.eventSource.onmessage = (event) => {
      try {
        const serverEvent: ServerEvent<T> = JSON.parse(event.data);
        this.emit(serverEvent.type, serverEvent.data);
      } catch (error) {
        console.error('Failed to parse SSE data:', error);
      }
    };
  }
  
  on(eventType: string, callback: (data: T) => void): void {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, new Set());
    }
    
    this.listeners.get(eventType)!.add(callback);
  }
  
  off(eventType: string, callback: (data: T) => void): void {
    const callbacks = this.listeners.get(eventType);
    if (callbacks) {
      callbacks.delete(callback);
    }
  }
  
  private emit(eventType: string, data: T): void {
    const callbacks = this.listeners.get(eventType);
    if (callbacks) {
      callbacks.forEach(callback => callback(data));
    }
  }
  
  disconnect(): void {
    if (this.eventSource) {
      this.eventSource.close();
      this.eventSource = null;
      console.log('SSE connection closed');
    }
  }
}

// Usage
const sseClient = new SSEClient<NotificationEvent>('/api/notifications/stream');

// Add event listeners
sseClient.on('notification', (notification) => {
  console.log(`New notification: ${notification.message}`);
  
  if (notification.type === 'error') {
    alert(notification.message);
  }
});

sseClient.on('update', (data) => {
  console.log('Data updated:', data);
});

// Connect to server
sseClient.connect();

// Later, disconnect
// sseClient.disconnect();

Test Your Knowledge

Type Safety Check

Which API response typing is correct?

// Option A
interface ApiResponse {
  data: any;
  status: number;
}

async function fetchUser() {
  const response = await fetch('/api/user');
  const data: ApiResponse = await response.json();
  return data.data;
}

// Option B
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  const data: User = await response.json();
  return data;
}

// Option C
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  const result: ApiResponse<User> = await response.json();
  
  if (!result.success) {
    throw new Error(result.error);
  }
  
  return result.data;
}

// Option D
async function fetchUser() {
  const response = await fetch('/api/user');
  return response.json();  // No typing
}

Common TypeScript Error

Code with Error
interface User {
  id: number;
  name: string;
  email: string;
}

// Unsafe API call - no validation
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: User = await response.json();
  // Just trusting the API response matches User interface!
  return data;
}

// Using the function
const user = await fetchUser(1);
console.log(user.name.toUpperCase());  // ❌ Runtime error if API returns different structure!

// What if API returns:
// { userId: 1, fullName: "Yusuf", emailAddress: "yusuf@example.com" }
// TypeScript won't catch this - it trusts your type assertion!

❌ TypeError: Cannot read property 'toUpperCase' of undefined (runtime error)

What's Wrong?

Just typing the response doesn't validate it! The API might return a different structure than your interface expects. TypeScript can't validate runtime JSON data.

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

// Type guard for runtime validation
function isUser(obj: any): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string'
  );
}

// ✅ Safe API call with validation
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  const data: unknown = await response.json();
  
  // Validate at runtime
  if (!isUser(data)) {
    throw new Error('Invalid user data structure from API');
  }
  
  return data;  // TypeScript knows this is User now
}

// ✅ Using the validated function
try {
  const user = await fetchUser(1);
  console.log(user.name.toUpperCase());  // ✅ Safe - validated
} catch (error) {
  console.error('Failed to fetch user:', error);
}

// ✅ Using Zod for validation (recommended)
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email()
});

type User = z.infer<typeof UserSchema>;

async function fetchUserWithZod(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // Zod validates and throws if invalid
  return UserSchema.parse(data);
}

Solution: Validate API responses at runtime

Best Practices

  1. Always type API responses - create interfaces for all data structures
  2. Validate data at runtime - don't trust API responses blindly
  3. Handle errors properly - check response.ok and status codes
  4. Use generic wrappers for consistent API response structures
  5. Create dedicated service classes for each API resource
  6. Add request/response interceptors for auth and error handling
  7. Set timeouts to prevent hanging requests
  8. Type query parameters and request bodies - prevent typos
best-practices-example.ts
// ✅ Good: Type all API responses
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('Failed');
  return response.json();
}

// ✅ Good: Generic API wrapper
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

async function apiGet<T>(url: string): Promise<T> {
  const response = await fetch(url);
  const result: ApiResponse<T> = await response.json();
  if (!result.success) throw new Error(result.error);
  return result.data;
}

// ✅ Good: Dedicated service classes
class UserService {
  constructor(private api: ApiClient) {}
  
  async getById(id: number): Promise<User> {
    return this.api.get<User>(`/users/${id}`);
  }
  
  async create(data: CreateUserDto): Promise<User> {
    return this.api.post<User>('/users', data);
  }
}

// ✅ Good: Runtime validation
function isUser(obj: any): obj is User {
  return (
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string'
  );
}

// ❌ Avoid: Using 'any' for responses
async function badFetch(): Promise<any> {
  return fetch('/api/data').then(r => r.json());
}

// ❌ Avoid: No error handling
async function alsoBad() {
  const response = await fetch('/api/data');
  return response.json();  // What if it fails?
}

// ❌ Avoid: No validation
async function trustingTooMuch(): Promise<User> {
  const data = await fetch('/api/user').then(r => r.json());
  return data;  // Blindly trusting API!
}

interface CreateUserDto {
  name: string;
  email: string;
}

class ApiClient {
  async get<T>(url: string): Promise<T> { return {} as T; }
  async post<T>(url: string, body: any): Promise<T> { return {} as T; }
}

Key Takeaways

  • Define interfaces for all API response structures
  • Use generic types <T> for flexible API clients
  • Validate data at runtime—TypeScript can't validate JSON
  • Always check response.ok before parsing
  • Create dedicated service classes for each API resource
  • Use interceptors for authentication and error handling
  • Type query parameters and request bodies
  • Handle timeouts and network errors gracefully
  • Consider using Axios for advanced features
  • Build reusable, type-safe API clients

What's Next?

Excellent work! You've mastered API integration and JSON handling with TypeScript. Next, we'll explore Type Declarations and DefinitelyTyped—how to use @types packages, create custom declaration files (.d.ts), and work with third-party libraries that don't have built-in TypeScript support!

Get ready to master type declarations!

Mastering TypeScript API integration! Check this out!

Previous
Async/Await and Promises
Next
Type Declarations and DefinitelyTyped

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo