Back to Blogs
18 min readApr 1, 2026

Next.js Data Fetching Guide: Axios + SWR + React Query + Zustand (Complete Setup)

Master data fetching in Next.js 14+. Complete setup guide for Axios, SWR, React Query, and Zustand with TypeScript. Includes best practices, security tips, and production-ready code examples.

nextjs data fetchingaxios react query setupswr vs react queryzustand state managementnextjs axios configurationreact query nextjs 14

Next.js Data Fetching Guide: Axios + SWR + React Query + Zustand (Complete Setup)

If you're building a Next.js app in 2026, you have too many options for data fetching and state management.

Should you use native fetch? Axios? SWR? React Query? Where does Zustand fit? What about Redux?

This guide cuts through the confusion with:

  • When to use each tool (decision tree included)
  • Complete production setup (copy-paste ready)
  • Best practices from real-world projects
  • How to use them together (the right way)

By the end, you'll have a bulletproof data fetching architecture for your Next.js app.

Quick Decision Tree: Which Tools Do You Need?

1. Do you need to fetch data from APIs?

Yes = You need a data fetching library

  • Simple APIs, caching needed = Use SWR
  • Complex queries, mutations, optimistic updates = Use React Query
  • No library needed = Use native fetch with Server Components

No = Skip to state management

2. Do you need complex request configuration (auth, retries, interceptors)?

Yes = Add Axios as your HTTP client
No = Stick with native fetch

3. Do you need client-side state management (UI state, user preferences)?

Yes = Choose based on complexity:

  • Simple state (toggles, filters) = Use Zustand
  • Complex state (normalized data, time-travel debugging) = Use Redux Toolkit
  • Just React = Use useState + Context API

The Stack Comparison Table

ToolPurposeSizeLearning CurveBest For
Native fetchHTTP requests0KBEasyServer Components, simple APIs
AxiosHTTP client13KBEasyRequest/response interceptors
SWRData fetching + cache4.5KBMediumReal-time data, auto-refresh
React QueryData fetching + cache12KBMediumComplex mutations, optimistic updates
ZustandClient state1KBEasyUI state, user preferences
Redux ToolkitClient state11KBHardComplex normalized state

Let's build each one from scratch.

Part 1: Setting Up Axios (The Foundation)

Why Use Axios Over Native Fetch?

Axios advantages:

  • Automatic JSON transformation
  • Request/response interceptors (auth tokens, logging)
  • Request cancellation
  • Better error handling
  • Progress tracking for uploads

When NOT to use Axios:

  • Server Components (use native fetch with caching)
  • You don't need interceptors

Installation

npm install axios# orpnpm add axios

Complete Axios Setup (Production-Ready)

// lib/api/axios.tsimport axios, {   AxiosError,   AxiosInstance,   InternalAxiosRequestConfig,  AxiosResponse } from 'axios'; // Create base instanceconst api: AxiosInstance = axios.create({  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',  timeout: 10000, // 10 seconds  headers: {    'Content-Type': 'application/json',  },}); // Request interceptor (add auth token, logging)api.interceptors.request.use(  (config: InternalAxiosRequestConfig) => {    // Add auth token if exists    const token = localStorage.getItem('auth_token');    if (token) {      config.headers.Authorization = `Bearer ${token}`;    }     // Log request in development    if (process.env.NODE_ENV === 'development') {      console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`);    }     return config;  },  (error: AxiosError) => {    console.error('[API Request Error]', error);    return Promise.reject(error);  }); // Response interceptor (handle errors globally)api.interceptors.response.use(  (response: AxiosResponse) => {    // Log response in development    if (process.env.NODE_ENV === 'development') {      console.log(`[API Response] ${response.status} ${response.config.url}`);    }    return response;  },  async (error: AxiosError) => {    const originalRequest = error.config;     // Handle 401 Unauthorized (token expired)    if (error.response?.status === 401 && originalRequest) {      try {        // Refresh token logic here        const newToken = await refreshAuthToken();                // Update token        localStorage.setItem('auth_token', newToken);                // Retry original request        originalRequest.headers.Authorization = `Bearer ${newToken}`;        return api(originalRequest);      } catch (refreshError) {        // Refresh failed, redirect to login        window.location.href = '/login';        return Promise.reject(refreshError);      }    }     // Handle 429 Too Many Requests (rate limit)    if (error.response?.status === 429) {      const retryAfter = error.response.headers['retry-after'];      console.warn(`Rate limited. Retry after ${retryAfter} seconds`);    }     // Log error    console.error('[API Error]', {      status: error.response?.status,      message: error.message,      url: error.config?.url,    });     return Promise.reject(error);  }); // Helper function for token refreshasync function refreshAuthToken(): Promise<string> {  const refreshToken = localStorage.getItem('refresh_token');    const response = await axios.post(    `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,    { refresh_token: refreshToken }  );    return response.data.access_token;} export default api;

Usage Example

// app/api-examples/basic-usage.tsimport api from '@/lib/api/axios'; // GET requestexport async function getUsers() {  const response = await api.get('/users');  return response.data;} // POST requestexport async function createUser(data: { name: string; email: string }) {  const response = await api.post('/users', data);  return response.data;} // PUT requestexport async function updateUser(id: string, data: Partial<User>) {  const response = await api.put(`/users/${id}`, data);  return response.data;} // DELETE requestexport async function deleteUser(id: string) {  const response = await api.delete(`/users/${id}`);  return response.data;}

Part 2: React Query Setup (Powerful Data Fetching)

Why React Query?

React Query excels at:

  • Automatic caching and background refetching
  • Optimistic updates
  • Pagination and infinite scrolling
  • Dependent queries
  • Mutation management

When to use React Query:

  • Complex data fetching needs
  • Real-time updates
  • Optimistic UI patterns

Installation

npm install @tanstack/react-query @tanstack/react-query-devtools# orpnpm add @tanstack/react-query @tanstack/react-query-devtools

Complete React Query Setup

// lib/react-query/client.tsimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'; export const queryClient = new QueryClient({  defaultOptions: {    queries: {      // Stale time: 5 minutes      staleTime: 5 * 60 * 1000,      // Cache time: 10 minutes      gcTime: 10 * 60 * 1000,      // Retry failed requests 3 times      retry: 3,      // Refetch on window focus      refetchOnWindowFocus: false,      // Refetch on reconnect      refetchOnReconnect: true,    },    mutations: {      // Retry failed mutations once      retry: 1,    },  },});
// app/providers.tsx'use client'; import { QueryClientProvider } from '@tanstack/react-query';import { ReactQueryDevtools } from '@tanstack/react-query-devtools';import { queryClient } from '@/lib/react-query/client'; export function Providers({ children }: { children: React.ReactNode }) {  return (    <QueryClientProvider client={queryClient}>      {children}      {process.env.NODE_ENV === 'development' && (        <ReactQueryDevtools initialIsOpen={false} />      )}    </QueryClientProvider>  );}
// app/layout.tsximport { Providers } from './providers'; export default function RootLayout({  children,}: {  children: React.ReactNode;}) {  return (    <html lang="en">      <body>        <Providers>{children}</Providers>      </body>    </html>  );}

Custom Hooks with React Query + Axios

// hooks/use-users.tsimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';import api from '@/lib/api/axios'; interface User {  id: string;  name: string;  email: string;} // Fetch all usersexport function useUsers() {  return useQuery({    queryKey: ['users'],    queryFn: async () => {      const response = await api.get<User[]>('/users');      return response.data;    },  });} // Fetch single userexport function useUser(id: string) {  return useQuery({    queryKey: ['users', id],    queryFn: async () => {      const response = await api.get<User>(`/users/${id}`);      return response.data;    },    enabled: !!id, // Only run if id exists  });} // Create user mutationexport function useCreateUser() {  const queryClient = useQueryClient();   return useMutation({    mutationFn: async (data: Omit<User, 'id'>) => {      const response = await api.post<User>('/users', data);      return response.data;    },    onSuccess: () => {      // Invalidate and refetch users list      queryClient.invalidateQueries({ queryKey: ['users'] });    },  });} // Update user mutationexport function useUpdateUser() {  const queryClient = useQueryClient();   return useMutation({    mutationFn: async ({ id, ...data }: Partial<User> & { id: string }) => {      const response = await api.put<User>(`/users/${id}`, data);      return response.data;    },    onSuccess: (data) => {      // Update cache optimistically      queryClient.setQueryData(['users', data.id], data);      // Invalidate users list      queryClient.invalidateQueries({ queryKey: ['users'] });    },  });} // Delete user mutationexport function useDeleteUser() {  const queryClient = useQueryClient();   return useMutation({    mutationFn: async (id: string) => {      await api.delete(`/users/${id}`);      return id;    },    onSuccess: (id) => {      // Remove from cache      queryClient.removeQueries({ queryKey: ['users', id] });      // Invalidate users list      queryClient.invalidateQueries({ queryKey: ['users'] });    },  });}

Usage in Components

// app/users/page.tsx'use client'; import { useUsers, useCreateUser, useDeleteUser } from '@/hooks/use-users'; export default function UsersPage() {  const { data: users, isLoading, error } = useUsers();  const createUser = useCreateUser();  const deleteUser = useDeleteUser();   async function handleCreate() {    await createUser.mutateAsync({      name: 'John Doe',      email: 'john@example.com',    });  }   async function handleDelete(id: string) {    await deleteUser.mutateAsync(id);  }   if (isLoading) return <div>Loading...</div>;  if (error) return <div>Error: {error.message}</div>;   return (    <div>      <button onClick={handleCreate}>Create User</button>            <ul>        {users?.map((user) => (          <li key={user.id}>            {user.name} - {user.email}            <button onClick={() => handleDelete(user.id)}>Delete</button>          </li>        ))}      </ul>    </div>  );}

Part 3: SWR Setup (Alternative to React Query)

Why SWR?

SWR advantages:

  • Smaller bundle size (4.5KB vs 12KB)
  • Simpler API
  • Built by Vercel (Next.js team)
  • Excellent for real-time data

When to use SWR:

  • You want something simpler than React Query
  • Real-time data with auto-revalidation
  • You're using Next.js

Installation

npm install swr# orpnpm add swr

SWR Configuration

// lib/swr/config.tsimport { SWRConfiguration } from 'swr';import api from '@/lib/api/axios'; // Global fetcher using Axiosexport const fetcher = async (url: string) => {  const response = await api.get(url);  return response.data;}; // SWR configurationexport const swrConfig: SWRConfiguration = {  fetcher,  revalidateOnFocus: true,  revalidateOnReconnect: true,  refreshInterval: 0, // Disable auto-refresh (set to 5000 for 5s polling)  dedupingInterval: 2000, // 2 seconds  errorRetryCount: 3,  errorRetryInterval: 5000,};
// app/providers.tsx (add SWR)'use client'; import { SWRConfig } from 'swr';import { swrConfig } from '@/lib/swr/config'; export function Providers({ children }: { children: React.ReactNode }) {  return (    <SWRConfig value={swrConfig}>      {children}    </SWRConfig>  );}

Usage with SWR

// hooks/use-users-swr.tsimport useSWR from 'swr';import useSWRMutation from 'swr/mutation';import api from '@/lib/api/axios'; interface User {  id: string;  name: string;  email: string;} // Fetch usersexport function useUsersSWR() {  return useSWR<User[]>('/users');} // Fetch single userexport function useUserSWR(id: string) {  return useSWR<User>(id ? `/users/${id}` : null);} // Create userexport function useCreateUserSWR() {  return useSWRMutation(    '/users',    async (url, { arg }: { arg: Omit<User, 'id'> }) => {      const response = await api.post<User>(url, arg);      return response.data;    }  );} // Delete userexport function useDeleteUserSWR() {  return useSWRMutation(    '/users',    async (url, { arg }: { arg: string }) => {      await api.delete(`${url}/${arg}`);      return arg;    }  );}

Part 4: Zustand Setup (Lightweight State Management)

Why Zustand?

Zustand advantages:

  • Tiny bundle size (1KB)
  • No boilerplate
  • TypeScript-friendly
  • Works outside React
  • Simple to learn

When to use Zustand:

  • UI state (modals, sidebars, filters)
  • User preferences (theme, language)
  • Temporary client-side data

Installation

npm install zustand# orpnpm add zustand

Complete Zustand Store

// stores/use-app-store.tsimport { create } from 'zustand';import { persist, createJSONStorage } from 'zustand/middleware'; interface User {  id: string;  name: string;  email: string;} interface AppState {  // Auth state  user: User | null;  isAuthenticated: boolean;  setUser: (user: User | null) => void;  logout: () => void;   // UI state  isSidebarOpen: boolean;  toggleSidebar: () => void;    theme: 'light' | 'dark';  setTheme: (theme: 'light' | 'dark') => void;   // Filter state  searchQuery: string;  setSearchQuery: (query: string) => void;} export const useAppStore = create<AppState>()(  persist(    (set) => ({      // Auth state      user: null,      isAuthenticated: false,      setUser: (user) => set({ user, isAuthenticated: !!user }),      logout: () => set({ user: null, isAuthenticated: false }),       // UI state      isSidebarOpen: true,      toggleSidebar: () => set((state) => ({         isSidebarOpen: !state.isSidebarOpen       })),       theme: 'light',      setTheme: (theme) => set({ theme }),       // Filter state      searchQuery: '',      setSearchQuery: (searchQuery) => set({ searchQuery }),    }),    {      name: 'app-storage', // LocalStorage key      storage: createJSONStorage(() => localStorage),      // Only persist specific fields      partialize: (state) => ({        user: state.user,        theme: state.theme,        isAuthenticated: state.isAuthenticated,      }),    }  ));

Usage in Components

// app/dashboard/page.tsx'use client'; import { useAppStore } from '@/stores/use-app-store'; export default function Dashboard() {  const { user, theme, setTheme, isSidebarOpen, toggleSidebar } = useAppStore();   return (    <div className={`dashboard theme-${theme}`}>      <button onClick={toggleSidebar}>        {isSidebarOpen ? 'Close' : 'Open'} Sidebar      </button>       <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>        Toggle Theme      </button>       <div>        <h1>Welcome, {user?.name}</h1>      </div>    </div>  );}

Zustand with Immer (for nested state)

npm install immer
// stores/use-cart-store.tsimport { create } from 'zustand';import { immer } from 'zustand/middleware/immer'; interface CartItem {  id: string;  name: string;  quantity: number;  price: number;} interface CartState {  items: CartItem[];  addItem: (item: CartItem) => void;  removeItem: (id: string) => void;  updateQuantity: (id: string, quantity: number) => void;  clearCart: () => void;  total: number;} export const useCartStore = create<CartState>()(  immer((set, get) => ({    items: [],     addItem: (item) => set((state) => {      const existingItem = state.items.find((i) => i.id === item.id);      if (existingItem) {        existingItem.quantity += item.quantity;      } else {        state.items.push(item);      }    }),     removeItem: (id) => set((state) => {      state.items = state.items.filter((item) => item.id !== id);    }),     updateQuantity: (id, quantity) => set((state) => {      const item = state.items.find((i) => i.id === id);      if (item) {        item.quantity = quantity;      }    }),     clearCart: () => set((state) => {      state.items = [];    }),     get total() {      return get().items.reduce(        (sum, item) => sum + item.price * item.quantity,        0      );    },  })));

Part 5: Redux Toolkit (For Complex State)

Installation

npm install @reduxjs/toolkit react-redux

Redux Toolkit Setup

// store/store.tsimport { configureStore } from '@reduxjs/toolkit';import authReducer from './slices/auth-slice';import cartReducer from './slices/cart-slice'; export const store = configureStore({  reducer: {    auth: authReducer,    cart: cartReducer,  },  middleware: (getDefaultMiddleware) =>    getDefaultMiddleware({      serializableCheck: false,    }),}); export type RootState = ReturnType<typeof store.getState>;export type AppDispatch = typeof store.dispatch;
// store/slices/auth-slice.tsimport { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface AuthState {  user: { id: string; name: string; email: string } | null;  token: string | null;  isAuthenticated: boolean;} const initialState: AuthState = {  user: null,  token: null,  isAuthenticated: false,}; const authSlice = createSlice({  name: 'auth',  initialState,  reducers: {    setUser: (state, action: PayloadAction<AuthState['user']>) => {      state.user = action.payload;      state.isAuthenticated = !!action.payload;    },    setToken: (state, action: PayloadAction<string>) => {      state.token = action.payload;    },    logout: (state) => {      state.user = null;      state.token = null;      state.isAuthenticated = false;    },  },}); export const { setUser, setToken, logout } = authSlice.actions;export default authSlice.reducer;

Part 6: Combining Everything (The Right Way)

Architecture Overview

┌─────────────────────────────────────┐│         Next.js App                 │├─────────────────────────────────────┤│  Server Components (fetch)          │ ← Use native fetch with caching├─────────────────────────────────────┤│  Client Components                  ││  ├─ React Query / SWR               │ ← Server state (API data)│  │  └─ Axios (HTTP client)          │ ← Request/response handling│  ├─ Zustand                         │ ← Client state (UI, preferences)│  └─ Redux Toolkit (optional)        │ ← Complex normalized state└─────────────────────────────────────┘

Complete Integration Example

// app/products/page.tsx'use client'; import { useProducts, useCreateProduct } from '@/hooks/use-products';import { useAppStore } from '@/stores/use-app-store';import { useCartStore } from '@/stores/use-cart-store'; export default function ProductsPage() {  // Server state (React Query + Axios)  const { data: products, isLoading } = useProducts();  const createProduct = useCreateProduct();   // Client state (Zustand)  const { searchQuery, setSearchQuery } = useAppStore();  const { addItem } = useCartStore();   // Filter products based on search  const filteredProducts = products?.filter((product) =>    product.name.toLowerCase().includes(searchQuery.toLowerCase())  );   return (    <div>      <input        type="text"        placeholder="Search products..."        value={searchQuery}        onChange={(e) => setSearchQuery(e.target.value)}      />       {isLoading && <div>Loading...</div>}       <ul>        {filteredProducts?.map((product) => (          <li key={product.id}>            {product.name} - ${product.price}            <button onClick={() => addItem({ ...product, quantity: 1 })}>              Add to Cart            </button>          </li>        ))}      </ul>    </div>  );}

Best Practices & Common Patterns

1. Error Handling Pattern

// hooks/use-products.tsexport function useProducts() {  return useQuery({    queryKey: ['products'],    queryFn: async () => {      try {        const response = await api.get('/products');        return response.data;      } catch (error) {        if (axios.isAxiosError(error)) {          throw new Error(            error.response?.data?.message || 'Failed to fetch products'          );        }        throw error;      }    },  });}

2. Optimistic Updates Pattern

export function useUpdateProduct() {  const queryClient = useQueryClient();   return useMutation({    mutationFn: async (product: Product) => {      const response = await api.put(`/products/${product.id}`, product);      return response.data;    },    onMutate: async (newProduct) => {      // Cancel outgoing refetches      await queryClient.cancelQueries({ queryKey: ['products'] });       // Snapshot previous value      const previousProducts = queryClient.getQueryData(['products']);       // Optimistically update      queryClient.setQueryData(['products'], (old: Product[] = []) =>        old.map((p) => (p.id === newProduct.id ? newProduct : p))      );       return { previousProducts };    },    onError: (err, newProduct, context) => {      // Rollback on error      queryClient.setQueryData(['products'], context?.previousProducts);    },    onSettled: () => {      // Refetch after error or success      queryClient.invalidateQueries({ queryKey: ['products'] });    },  });}

3. Pagination Pattern

export function useProductsPaginated(page: number) {  return useQuery({    queryKey: ['products', 'paginated', page],    queryFn: async () => {      const response = await api.get(`/products?page=${page}&limit=10`);      return response.data;    },    keepPreviousData: true, // Keep old data while fetching new page  });}

4. Infinite Scroll Pattern

import { useInfiniteQuery } from '@tanstack/react-query'; export function useProductsInfinite() {  return useInfiniteQuery({    queryKey: ['products', 'infinite'],    queryFn: async ({ pageParam = 1 }) => {      const response = await api.get(`/products?page=${pageParam}&limit=10`);      return response.data;    },    getNextPageParam: (lastPage, pages) => {      return lastPage.hasMore ? pages.length + 1 : undefined;    },  });}

Performance Optimization Tips

1. Request Deduplication

React Query and SWR automatically deduplicate requests made within a short time window.

// Multiple components calling this simultaneously = single requestconst { data } = useUsers();

2. Prefetching Data

// Prefetch on hoverfunction ProductLink({ id }: { id: string }) {  const queryClient = useQueryClient();   return (    <Link      href={`/products/${id}`}      onMouseEnter={() => {        queryClient.prefetchQuery({          queryKey: ['product', id],          queryFn: () => fetchProduct(id),        });      }}    >      View Product    </Link>  );}

3. Selective Subscriptions (Zustand)

// Only re-render when theme changesconst theme = useAppStore((state) => state.theme); // NOT this (re-renders on any state change)const { theme } = useAppStore();

When to Use What: The Complete Decision Matrix

Common Questions

Common Mistakes to Avoid

1. Mixing Server and Client State in One Place

2. Not Handling Loading States

// Badconst { data } = useUsers();return <div>{data.map(...)}</div>; // Crashes if data is undefined // Goodconst { data, isLoading, error } = useUsers();if (isLoading) return <Spinner />;if (error) return <ErrorMessage error={error} />;return <div>{data?.map(...)}</div>;

3. Over-fetching Data

// Bad: Fetching on every renderfunction Component() {  const [data, setData] = useState([]);    useEffect(() => {    fetchData().then(setData);  }); // Missing dependency array = infinite loop} // Good: Use React Queryfunction Component() {  const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });}

Complete Project Structure

my-nextjs-app/├── app/│   ├── providers.tsx          # React Query + SWR providers│   ├── layout.tsx│   └── page.tsx├── lib/│   ├── api/│   │   └── axios.ts           # Axios instance + interceptors│   ├── react-query/│   │   └── client.ts          # React Query config│   └── swr/│       └── config.ts          # SWR config├── stores/│   ├── use-app-store.ts       # Global UI state│   └── use-cart-store.ts      # Cart state├── hooks/│   ├── use-users.ts           # User queries + mutations│   └── use-products.ts        # Product queries + mutations└── types/    └── index.ts               # TypeScript types

Production Checklist

Security Best Practices

1. Never Store Tokens in LocalStorage (Production)

// Bad (vulnerable to XSS)localStorage.setItem('token', token); // Better: Use httpOnly cookies (set by server)// Or use secure session management library

2. Sanitize User Input

import DOMPurify from 'isomorphic-dompurify'; const sanitized = DOMPurify.sanitize(userInput);

3. Implement Rate Limiting

// In your API routesimport rateLimit from 'express-rate-limit'; const limiter = rateLimit({  windowMs: 15 * 60 * 1000, // 15 minutes  max: 100, // Limit each IP to 100 requests per window}); app.use('/api/', limiter);

Debugging Tips

React Query Devtools

// Open devtoolsimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'; <ReactQueryDevtools initialIsOpen={false} />

Zustand Devtools

npm install zustand-devtools
import { devtools } from 'zustand/middleware'; const useStore = create(  devtools((set) => ({    // your state  })));

Axios Request Logging

// Already included in our Axios setupapi.interceptors.request.use((config) => {  console.log('[API Request]', config.method?.toUpperCase(), config.url);  return config;});

Further Reading

Official Documentation

Coming soon:

  • Next.js Server Components vs Client Components: When to Use What
  • Building a Real-Time Dashboard with Next.js + React Query
  • Complete Next.js Authentication Guide (2026)

Conclusion

You don't need all these tools. Here's what most Next.js apps actually need:

Minimal Stack (90% of projects):

  • Native fetch in Server Components
  • React Query + Axios for Client Components
  • Zustand for UI state

Full Stack (Complex apps):

  • Add SWR for real-time features
  • Add Redux Toolkit only if team requires it

The key: Start simple, add complexity only when needed.

Copy the setups from this guide, adjust to your needs, and ship.

Need help architecting your Next.js application? We build production-ready SaaS products with modern tech stacks. Contact Websyro Agency for expert consultation.

Related Blogs

View all