How to Use React Query in Next.js with Axios & Type Safety
React Query

How to Use React Query in Next.js with Axios & Type Safety

Sagar Yenkure
July 20th, 2025

If you're building a modern Next.js app, managing API state and server cache effectively is crucial. That's where React Query (now called TanStack Query) comes in.

In this guide, you'll learn how to set up React Query with Axios in a scalable structure:

  • Axios client setup
  • Services for API calls
  • Feature-based folders for hooks
  • React Query Client in the layout/app

Folder Structure

📦 app ┣ 📂 layout.tsx → Wraps app in QueryClientProvider ┣ 📂 services ┃ ┗ 📄 user.service.ts → Axios-based API calls ┣ 📂 features ┃ ┗ 📄 useUser.ts → useQuery / useMutation hooks ┣ 📂 lib ┃ ┗ 📄 axios.ts → Axios instance
Click to Copy

Step 1: Install Dependencies

npm install @tanstack/react-query axios
Click to Copy

Step 2: Create Axios Instance

// lib/axios.ts import axios from "axios"; const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE || "https://localhost:3000/api", headers: { "Content-Type": "application/json", }, }); export default api;
Click to Copy

Step 3: Service Layer (API Calls)

// services/user.service.ts import api from "@/lib/axios"; export const getUsers = async () => { const { data } = await api.get("/users"); return data; }; export const createUser = async (payload: { name: string; email: string }) => { const { data } = await api.post("/users", payload); return data; };
Click to Copy

Step 4: React Query Hooks in Features

// features/useUser.ts import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getUsers, createUser } from "@/services/user.service"; export const useUsers = () => { return useQuery({ queryKey: ["users"], // This key identifies the query queryFn: getUsers, }); }; export const useCreateUser = () => { const queryClient = useQueryClient(); const { mutate, isPending, isError, } = useMutation({ mutationFn: createUser, onSuccess: () => { // Tell React Query to refetch the "users" query // This ensures the UI shows the latest data after a new user is added queryClient.invalidateQueries({ queryKey: ["users"] }); }, }); return { mutate, isPending, isError }; };
Click to Copy

Step 5: Provide React Query in Layout

// app/layout.tsx (for App Router) // OR pages/_app.tsx (for Pages Router) "use client"; import { ReactNode } from "react"; import { QueryClient, QueryClientProvider, } from "@tanstack/react-query"; const queryClient = new QueryClient(); export default function RootLayout({ children }: { children: ReactNode }) { return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); }
Click to Copy

Step 6: Use Hook in Component

"use client"; import { useUsers, useCreateUser } from "@/features/useUser"; export default function UserPage() { const { data: users, isLoading } = useUsers(); const { mutate, isPending, isError } = useCreateUser(); const handleAdd = () => { mutate({ name: "John", email: "john@example.com" }); }; if (isLoading) return <p>Loading...</p>; if (isError) return <p>Error fetching users</p>; return ( <div> <h2>Users</h2> <button onClick={handleAdd} disabled={isPending}> {isPending ? "Creating..." : "Add User"} </button> <ul> {users?.map((u: any) => ( <li key={u.id}>{u.name}</li> ))} </ul> </div> ); }
Click to Copy

Final Thoughts

With this setup:

✅ API logic is isolated in services
✅ React Query hooks live in features
✅ Axios is reused via an instance
✅ Client is wrapped once in the layout

This structure is perfect for scaling apps with clean boundaries and predictable behavior.

You can now use React Query to manage data fetching, mutation, caching, and sync across your entire app.