Skip to content

A learning/sample project demonstrating best practices for API-driven React apps: axios API-layer separation, axios interceptors with automatic refresh-token flow, and server-state management using React Query. Includes a small Node + Express mock API with hard-coded dummy data (dockerized) for easy testing.

Notifications You must be signed in to change notification settings

Dimuthu7/react-api-handling-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

react-api-handling-guide

A clean, production-style learning project demonstrating how to properly handle API calls in React using:

  • Axios API layer separation
  • Axios interceptors with automatic refresh-token flow
  • React Query for server-state, caching & mutations
  • React Hook Form + Zod for form validation
  • Zustand for small client-side state
  • TailwindCSS for styling
  • A Node + Express mock backend (with dummy data) for easy testing
  • Backend fully dockerized for plug-and-play usage

This project is ideal for developers who want to understand proper architecture for API-driven React applications.


⭐ Why this project exists

The goal is to teach how to build React apps that communicate with APIs in a clean, scalable, production-friendly way:

  • How to separate your entire API logic into a dedicated API layer
  • How to configure Axios interceptors to auto-inject access tokens
  • How to automatically refresh access tokens using refresh tokens
  • How to avoid duplicate refresh calls using a request queue
  • How to implement proper server-state management using React Query
  • How to centralize validation using Zod schemas
  • How to keep UI minimal and clean using TailwindCSS

📁 Project Structure

Frontend (sample-app/)

src/
├─ api/                 # axios client + productApi, authApi, etc.
├─ hooks/               # React Query hooks (useProductsQuery, useCreateProduct...)
├─ pages/               # ProductList, ProductCreate, ProductEdit, Login
├─ components/          # ProductCard, layout components
├─ schemas/             # zod schemas for form validation
├─ store/               # Zustand (small UI/auth state)
├─ utils/               # global error handler + token helper
└─ main.tsx

Backend (backend-api/)

src/
├─ controllers/
├─ middlewares/         # auth middleware, error middleware
├─ services/
├─ routes/
├─ utils/               # dummyData.js, JWT helpers
├─ app.js
└─ server.js
docker-compose.yml
Dockerfile

🧱 API Layer Architecture (Why this matters)

All API calls are handled by:

1️⃣ axiosClient.ts

Centralized axios instance:

  • Base URL
  • JSON headers
  • Access-token injection (request interceptor)
    axiosClient.interceptors.request.use((config) => {
      const token = getAccessToken(); // get access token from local storage
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
  • Auto refresh-token handling (response interceptor)
  • Queues requests while refreshing
  • Prevents infinite refresh loops
  • Automatically retries failed requests after token refresh
    axiosClient.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;
    
        // ⛔️ STOP auto-refresh for login endpoint
        if (originalRequest?.url?.includes("/auth/login")) {
          return Promise.reject(error);
        }
    
        // If token expired
        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
    
          const refreshToken = getRefreshToken();
          if (!refreshToken) {
            window.location.href = "/login";
            return Promise.reject(error);
          }
    
          // Prevent multiple refresh requests
          if (isRefreshing) {
            return new Promise((resolve, reject) => {
              failedQueue.push({ resolve, reject });
            })
              .then((token) => {
                originalRequest.headers.Authorization = "Bearer " + token;
                return axiosClient(originalRequest);
              })
              .catch((err) => Promise.reject(err));
          }
    
          isRefreshing = true;
          try {
            const res = await axios.post(
              `${BASE_URL}/auth/refresh`,
              { refreshToken }
            );
            const newToken = res.data.accessToken;
            setAccessToken(newToken);
            axiosClient.defaults.headers.common.Authorization = "Bearer " + newToken;
    
            processQueue(null, newToken);
            return axiosClient(originalRequest);
          } catch (err) {
            processQueue(err, null);
            window.location.href = "/login";
            return Promise.reject(err);
          } finally {
            isRefreshing = false;
          }
        }
    
        return Promise.reject(error);
      }

2️⃣ API modules (productApi.ts, authApi.ts)

Each file holds high-level API functions:

// productApi.ts
export default {
  getAll: () => axiosClient.get("/products"),
  getById: (id: string) => axiosClient.get(`/products/${id}`),
  create: (data: Product) => axiosClient.post("/products", data),
  update: (id: string, data: Product) => axiosClient.put(`/products/${id}`, data),
  delete: (id: string) => axiosClient.delete(`/products/${id}`),
}

3️⃣ React Query Hooks

Examples:

useLogin()
useProductsQuery()
useCreateProduct()
useUpdateProduct()
useDeleteProduct()

This completely removes useEffect + manual loading/error state boilerplate.


🔐 Automatic Refresh Token Handling

This project implements a proper, production-ready refresh token flow:

✔ When access token expires

Backend returns 401

✔ Axios interceptor detects it

Skips login/refresh routes

✔ Uses stored refresh token

Calls /auth/refresh

✔ Gets new access token

Saves it → updates axios headers

✔ Retries all queued requests

User never sees an interruption

✔ If refresh token is invalid/expired

Redirect to /login and clear tokens


⚛️ Why React Query?

React Query solves server-state problems:

  • Auto caching
  • Automatic refetch on focus
  • Mutation hooks
  • Cache invalidation
  • Built-in loading/error states
  • Retry logic
  • Devtools to inspect queries

It removes the need for:

useEffect()
useState()
loading flags
error flags
dependency arrays

Your components stay clean and UI-focused.


🧪 Mock Backend (Node + Express)

A simple Express API using dummy in-memory data simulates:

  • JWT Login
  • Access & Refresh token issuing
  • Product CRUD
  • Protected routes
  • No database required
  • Dockerized for quick start

This is perfect for frontend API learning & testing.


🚀 How to Run (Step-by-step)

1️⃣ Backend (Docker)

cd backend-api
docker compose up --build

Or run manually:

npm install
npm run dev

Backend runs on:
👉 http://localhost:5000


2️⃣ Frontend (Vite)

cd sample-app
npm install
npm run dev

Frontend runs on:
👉 http://localhost:5173


🔧 Environment Variables

Frontend (sample-app/.env)

VITE_API_URL=http://localhost:5000

Backend (backend-api/.env)

PORT=5000
JWT_ACCESS_SECRET=your_access_secret
JWT_REFRESH_SECRET=your_refresh_secret
ACCESS_TOKEN_EXPIRE=15m
REFRESH_TOKEN_EXPIRE=7d

📦 Dependencies Used (Frontend)

Package Reason
react-hook-form Fast, minimal form handling
zod Strict TypeScript-first validation schemas
@hookform/resolvers Connects Zod to React Hook Form
axios HTTP client + interceptors for tokens
@tanstack/react-query Server-state management & caching
@tanstack/react-query-devtools Debug query cache in dev
zustand Tiny state store for UI/auth state
tailwindcss Utility-first CSS framework
sonner Toast notifications

💡 Next Steps

  • Add pagination with React Query
  • Add optimistic updates
  • Add image uploads
  • Add authentication roles (admin/user)
  • Switch refresh token storage to httpOnly cookies
  • Add unit tests (Vitest / Jest)
  • Add Docker for frontend
  • Add CI/CD workflow via GitHub Actions

📜 License

MIT — Free to use for learning or personal projects.


🎉 Happy Coding!

About

A learning/sample project demonstrating best practices for API-driven React apps: axios API-layer separation, axios interceptors with automatic refresh-token flow, and server-state management using React Query. Includes a small Node + Express mock API with hard-coded dummy data (dockerized) for easy testing.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published