Site icon WP Pluginsify

TypeScript Axios Response Types: Practical Patterns Using Axios, Fetch, and TanStack Query

Type your API boundary first, then let Axios, Fetch, or TanStack Query carry that type through the app. Axios is still widely used for typed HTTP work, and npm lists it with tens of millions of weekly downloads [source: npm]. That popularity does not remove the main risk: a response type can look safe while the server sends something else.

TLDR: Use AxiosResponse<T> when you need headers, status codes, or raw response data, but return plain T from service functions when UI code only needs data. For example, getUser(): Promise<User> is cleaner than forcing every component to handle response.data. In a dashboard with 12 API calls, this pattern can remove dozens of repeated .data reads and reduce type noise by roughly 30% in component files. Pair it with runtime validation for critical flows, because TypeScript does not inspect real JSON at runtime.

Start with the response shape

Good typing starts with the object you expect from the API. Keep it close to the API client code. That makes reviews easier and stops types from drifting away from real endpoints.

type User = {
  id: string;
  email: string;
  name: string;
};

type ApiError = {
  message: string;
  code?: string;
};

For Axios, the common pattern is simple:

import axios, { AxiosResponse } from "axios";

async function getUserRaw(id: string): Promise<AxiosResponse<User>> {
  return axios.get<User>(`/api/users/${id}`);
}

This is useful when you need status, headers, or full response metadata. But most UI code does not need that. It needs the user.

async function getUser(id: string): Promise<User> {
  const response = await axios.get<User>(`/api/users/${id}`);
  return response.data;
}

This second pattern is usually better for app code. It hides the HTTP client detail. If you swap Axios for Fetch later, fewer files change.

Image not found in postmeta

Axios response types that do not fight you

The catch is that Axios generics can give a false sense of safety. This code says the server returns a User. It does not prove it.

const response = await axios.get<User>("/api/me");

If the API sends { user_name: "Ana" }, TypeScript will not complain. It never saw the network response. For internal tools, that may be acceptable. For payments, auth, or medical data, it is not.

A practical setup looks like this:

It drives me crazy when every React component reads response.data.data.items. That is not type safety. That is plumbing leaked into the UI.

Typing Fetch without losing clarity

Fetch does not provide response generics. You need to create a small helper. Keep it strict and boring.

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

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

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

async function getProjects(): Promise<Project[]> {
  return apiFetch<Project[]>("/api/projects");
}

This mirrors the clean Axios service pattern. The component gets Project[], not a raw network object. That makes testing easier too. Mock the function result, not the entire browser response.

Using TanStack Query with typed functions

TanStack Query works best when your fetcher is already typed. The query does not need to guess. Its docs also show strong TypeScript support for query results and errors [source: TanStack].

function useUser(id: string) {
  return useQuery({
    queryKey: ["user", id],
    queryFn: () => getUser(id),
  });
}

If getUser returns Promise<User>, then data becomes User | undefined. That is exactly what the UI needs to know. The undefined state reflects loading, not a broken type.

const { data, isLoading, error } = useUser("42");

if (isLoading) return <p>Loading...</p>;
if (error) return <p>Could not load user.</p>;

return <p>{data?.name}</p>;

Handle errors on purpose

Axios errors need careful typing. Do not assume every caught error is an Axios error.

import axios from "axios";

try {
  await getUser("42");
} catch (error) {
  if (axios.isAxiosError<ApiError>(error)) {
    console.log(error.response?.data.message);
  }
}

This pattern avoids unsafe casts. It also handles network errors, where response may be missing. Expect to waste time on this if you treat all errors as the same object.

Recommended pattern for serious apps

  1. Create one API client for Axios or Fetch.
  2. Write one function per endpoint, such as getUser or createInvoice.
  3. Return domain data, not raw response wrappers.
  4. Use TanStack Query for cache, loading state, retries, and refetching.
  5. Add runtime validation for endpoints where bad data is costly.

Use AxiosResponse<T> with intent. It is not the default return type for every function. Most of the time, Promise<T> is cleaner, safer for UI code, and easier to replace later. Serious TypeScript work is not about adding more types. It is about putting the right type at the right boundary.

Exit mobile version