Skip to main content

🧱 Part 7 — HTTP & Data Fetching with Axios (Beginner‑Friendly, Fully Typed) 🌐


Real apps talk to servers. In this part, we’ll build a clean, typed HTTP layer using Axios and wire it to our UI. We’ll explain every piece and add comments right in the code so it’s copy‑paste ready.

🎯 Goals

  • One shared Axios client with sensible defaults
  • Typed request/response/error shapes
  • DTO ↔ Domain mapping (convert server shapes to UI‑friendly types)
  • Cancellation with AbortController
  • A tiny retry helper (with exponential backoff)
  • A commented TaskList that fetches & toggles tasks
  • Bonus: a minimal mock for local testing

0) Install (if needed)

pnpm add axios
pnpm add -D axios-mock-adapter   # optional, for local API mocks

1) Shared Axios client (one place for baseURL, headers, errors)

// src/lib/api/http.ts
import axios from 'axios';

// A simple, typed error shape we’ll use across the app
export type ApiError = { status: number; message: string };

// Create a single Axios instance used everywhere
export const http = axios.create({
  baseURL: import.meta.env.VITE_API_URL, // set in .env (e.g., http://localhost:3000)
  timeout: 10_000,                       // avoid hanging forever (10s)
  withCredentials: false,                // set true if your API uses cookies
});

// Attach auth token (if you add auth later)
http.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth:token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Normalize errors so components don’t need to know Axios internals
http.interceptors.response.use(
  (r) => r,
  (err) => {
    const status = err.response?.status ?? 0;
    const message = err.response?.data?.message ?? err.message;
    return Promise.reject({ status, message } as ApiError);
  },
);

Why: You avoid repeating base URL, headers, and error parsing in each API call.


2) DTO ↔ Domain mapping (UI shouldn’t care about server quirks)

// src/lib/api/tasks.types.ts
// Server shape (DTO). Servers often use snake_case and string dates.
export type TaskDTO = {
  id: string;
  title: string;
  done: boolean;
  created_at: string; // ISO string like "2025-08-26T10:00:00Z"
};

// UI shape (Domain). We prefer camelCase and real Date objects.
export type Task = {
  id: string;
  title: string;
  done: boolean;
  createdAt: Date;
};

export const toDomain = (d: TaskDTO): Task => ({
  id: d.id,
  title: d.title,
  done: d.done,
  createdAt: new Date(d.created_at),
});

// For create/update we only send what server expects
export type TaskCreate = { title: string };
export type TaskPatch = Partial<{ title: string; done: boolean }>;

3) Typed API functions (list/get/create/toggle/delete) + cancellation

// src/lib/api/tasks.ts
import { http, type ApiError } from './http';
import type { TaskDTO, Task, TaskCreate, TaskPatch } from './tasks.types';
import { toDomain } from './tasks.types';

// List tasks (with basic pagination) — supports AbortController via opts.signal
export async function listTasks(opts?: {
  page?: number;
  pageSize?: number;
  signal?: AbortSignal;
}): Promise<{ items: Task[]; total: number }> {
  const res = await http.get<{ items: TaskDTO[]; total: number }>(
    '/tasks',
    { params: { page: opts?.page ?? 1, pageSize: opts?.pageSize ?? 10 }, signal: opts?.signal },
  );
  return { items: res.data.items.map(toDomain), total: res.data.total };
}

export async function getTask(id: string, signal?: AbortSignal): Promise<Task> {
  const res = await http.get<TaskDTO>(`/tasks/${id}`, { signal });
  return toDomain(res.data);
}

export async function createTask(body: TaskCreate): Promise<Task> {
  const res = await http.post<TaskDTO>('/tasks', body);
  return toDomain(res.data);
}

export async function updateTask(id: string, patch: TaskPatch): Promise<Task> {
  const res = await http.patch<TaskDTO>(`/tasks/${id}`, patch);
  return toDomain(res.data);
}

export async function toggleTask(id: string): Promise<Task> {
  const res = await http.patch<TaskDTO>(`/tasks/${id}/toggle`);
  return toDomain(res.data);
}

export async function deleteTask(id: string): Promise<void> {
  await http.delete(`/tasks/${id}`);
}

Components only import these functions — never Axios directly — which keeps UI code tidy and testable.


4) Retry helper (optional but handy)

// src/lib/api/retry.ts
export async function retry<T>(fn: () => Promise<T>, attempts = 3, baseMs = 300): Promise<T> {
  let last: unknown;
  for (let i = 0; i < attempts; i++) {
    try { return await fn(); }
    catch (e) {
      last = e;
      const wait = baseMs * 2 ** i + Math.random() * 100; // backoff + jitter
      await new Promise((r) => setTimeout(r, wait));
    }
  }
  throw last; // bubble the last error
}

Use it anywhere:

import { retry } from './retry';
import { listTasks } from './tasks';

const { items } = await retry(() => listTasks({ page: 1 }));

In Part 8, TanStack Query will provide built‑in retries and caching.


5) A simple data‑fetching hook with cancellation

// src/features/tasks/useTasksFetch.tsx
import { useEffect, useState } from 'react';
import { listTasks, toggleTask, type Task } from '@/lib/api/tasks';
import type { ApiError } from '@/lib/api/http';

export function useTasksFetch(page = 1) {
  const [items, setItems] = useState<Task[]>([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<ApiError | null>(null);

  useEffect(() => {
    const ctrl = new AbortController(); // cancel if component unmounts or page changes
    setLoading(true);
    listTasks({ page, pageSize: 10, signal: ctrl.signal })
      .then(({ items, total }) => { setItems(items); setTotal(total); })
      .catch((e) => { if ((e as ApiError).message) setError(e as ApiError); })
      .finally(() => setLoading(false));
    return () => ctrl.abort();
  }, [page]);

  // convenience action with local optimistic update
  const flip = async (id: string) => {
    setItems((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
    try { const updated = await toggleTask(id);
      setItems((prev) => prev.map((t) => (t.id === id ? updated : t)));
    } catch (e) {
      // rollback on error
      setItems((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
      throw e;
    }
  };

  return { items, total, loading, error, flip } as const;
}

6) UI wiring (commented TaskList)

// src/features/tasks/TaskList.tsx
import { useState } from 'react';
import { useTasksFetch } from './useTasksFetch';

export function TaskList() {
  const [page, setPage] = useState(1);
  const { items, total, loading, error, flip } = useTasksFetch(page);

  if (loading) return <p>Loading…</p>;
  if (error) return <p className="text-red-600">❌ {error.message}</p>;

  return (
    <section>
      <h2 className="text-xl font-semibold mb-2">Tasks</h2>
      <ul className="space-y-2">
        {items.map((t) => (
          <li key={t.id} className="flex items-center gap-2">
            <input type="checkbox" checked={t.done} onChange={() => void flip(t.id)} />
            <span>{t.title}</span>
            <span className="text-gray-500 text-sm ml-auto">
              {t.createdAt.toLocaleDateString()}
            </span>
          </li>
        ))}
      </ul>
      <div className="mt-3 flex items-center gap-2">
        <button
          className="border rounded px-3 py-1"
          onClick={() => setPage((p) => Math.max(1, p - 1))}
          disabled={page === 1}
        >Prev</button>
        <span className="text-sm">Page {page}</span>
        <button
          className="border rounded px-3 py-1"
          onClick={() => setPage((p) => p + 1)}
          disabled={items.length === 0 || items.length + (page - 1) * 10 >= total}
        >Next</button>
      </div>
    </section>
  );
}

7) (Optional) Local mocking for demos/tests

// src/lib/api/mock.ts (dev only)
import MockAdapter from 'axios-mock-adapter';
import { http } from './http';

export function installMock() {
  const mock = new MockAdapter(http, { delayResponse: 300 });
  let tasks = [
    { id: '1', title: 'Buy milk', done: false, created_at: new Date().toISOString() },
    { id: '2', title: 'Write blog', done: true, created_at: new Date().toISOString() },
  ];

  mock.onGet('/tasks').reply((config) => {
    const page = Number((config.params?.page ?? 1));
    const pageSize = Number((config.params?.pageSize ?? 10));
    const start = (page - 1) * pageSize;
    const items = tasks.slice(start, start + pageSize);
    return [200, { items, total: tasks.length }];
  });

  mock.onPost('/tasks').reply((config) => {
    const body = JSON.parse(config.data);
    const t = { id: crypto.randomUUID(), title: body.title, done: false, created_at: new Date().toISOString() };
    tasks.unshift(t);
    return [201, t];
  });

  mock.onPatch(/\/tasks\/[^/]+\/toggle/).reply((config) => {
    const id = config.url!.split('/').pop()!; // naive parse for demo
    tasks = tasks.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
    const updated = tasks.find((t) => t.id === id)!;
    return [200, updated];
  });
}

Call installMock() once during development (e.g., in main.tsx) to run the app without a real backend.


8) Common pitfalls & fixes

  • Leaking requests: always abort in useEffect cleanup.
  • UI uses server shapes: map DTO → Domain early; keep dates/enums nice in the UI.
  • Sprinkling Axios in components: keep all HTTP in /lib/api functions.
  • Forgetting timeouts: set a reasonable timeout so you can show helpful errors.

✅ Wrap‑Up

You now have a solid, beginner‑friendly HTTP layer:

  • Central Axios client with typed ApiError
  • Clean DTO ↔ Domain mapping
  • Cancellable fetches + a small retry helper
  • A commented TaskList that fetches and toggles items

👉 Next up: Part 8 — Server State with TanStack Query for caching, retries, background refetch, and optimistic updates — all fully typed.


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...