DEV Community

Cover image for API Integration with Tanstack Query: Transforming Product Data Management
Yug Jadvani
Yug Jadvani

Posted on

API Integration with Tanstack Query: Transforming Product Data Management

In today’s fast‐paced digital economy, enterprise leaders must ensure that backend systems and user interfaces are seamlessly connected. Efficient API integration is no longer a technical afterthought it’s a strategic asset. Leveraging state‐of‐the‐art tools like Tanstack Query can significantly improve data consistency, reduce latency, and streamline product data management across large organizations.

Why Tanstack Query Matters for Enterprise Product Management

Tanstack Query excels in handling asynchronous server state, caching, and background data synchronization. For decision-makers, these features translate into:

  • Operational Agility: Real-time updates and reduced API overhead empower businesses to react swiftly to market changes.
  • Improved User Experience: Consistent data handling ensures that end users and internal teams alike work with the most up-to-date information.
  • Lower Maintenance Costs: By centralizing data fetching and error handling, development teams can reduce redundancy and mitigate risks associated with stale data.

Setting Up Tanstack Query and Dev Tools

Before diving into queries and mutations, it’s essential to set up Tanstack Query and its accompanying developer tools. Run the following commands in your project directory:

npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
Enter fullscreen mode Exit fullscreen mode

Next, initialize your QueryClient at the root of your application:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourAppComponents />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This setup is critical for efficient state management and debugging in production-level applications.

Code Walkthrough: Re-Ordered Product API Integration

Below is a series of re-ordered and annotated code examples that demonstrate how to integrate your product APIs with Tanstack Query. (Note: In these samples, every instance of “discount” has been replaced by “Product” to align with your current use-case.)


1. API Request Helper

This utility function standardizes API calls, providing consistent error handling and response parsing.

// api-request.ts
import { toast } from "sonner";

/**
 * Helper function for API requests.
 * Handles generic error messaging and returns JSON response.
 */
export const apiRequest = async (url: string, options: RequestInit) => {
  const response = await fetch(url, options);
  if (!response.ok) {
    const errorData = await response.json();
    toast.error(
      "Error: " + (errorData?.res?.message?.message || "Something went wrong")
    );
  }
  return response.json();
};
Enter fullscreen mode Exit fullscreen mode

2. Product API Functions

Re-ordered for clarity, these functions perform CRUD operations on product data. They use helper methods to retrieve company-specific tokens and IDs (analogous to host values).

// product-api.ts
import { Product } from "@/types/product"; // Custom type for Product data
import { Api } from "@/utils/api/api";
import { apiRequest } from "@/utils/api/api-request";
import { getCompanyId } from "@/utils/get-company-id"; // Similar to getHostId
import { getCompanyToken } from "@/utils/token-utils";   // Similar to getHostToken

const BASE_URL = `${Api}/products`;

/**
 * Fetch all active products for a company.
 */
export const getAllProductsApi = async () => {
  return apiRequest(`${BASE_URL}/company/${getCompanyId()}?is_active=true`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${getCompanyToken()}`,
    },
  });
};

/**
 * Fetch a single product by ID.
 */
export const getSingleProductApi = async (id: string) => {
  return apiRequest(`${BASE_URL}/${id}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${getCompanyToken()}`,
    },
  });
};

/**
 * Create a new product.
 */
export const createProductApi = async (data: Product) => {
  return apiRequest(BASE_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${getCompanyToken()}`,
    },
    body: JSON.stringify(data),
  });
};

/**
 * Update an existing product by ID.
 */
export const updateProductApi = async (id: string, data: Product) => {
  return apiRequest(`${BASE_URL}/${id}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${getCompanyToken()}`,
    },
    body: JSON.stringify(data),
  });
};

/**
 * Delete a product by ID.
 */
export const deleteProductApi = async (id: string) => {
  return apiRequest(`${BASE_URL}/${id}`, {
    method: "DELETE",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${getCompanyToken()}`,
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

3. Custom Hooks with Tanstack Query

These hooks abstract API integration logic using Tanstack Query’s useQuery and useMutation for optimal data fetching and mutation handling. Notice the consistent naming each hook is purpose-built for product operations.

// use-products.ts
import { Product } from "@/types/product";
import {
  createProductApi,
  getAllProductsApi,
  getSingleProductApi,
  updateProductApi,
  deleteProductApi,
} from "@/utils/api/product-api";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";

/**
 * Hook to fetch all products.
 */
export const useGetAllProducts = () => {
  return useQuery({
    queryKey: ["products"],
    queryFn: getAllProductsApi
  });
};

/**
 * Hook to fetch a single product by ID.
 */
export const useGetSingleProduct = (id: string) => {
  return useQuery({
    queryKey: ["product", id],
    queryFn: () => getSingleProductApi(id),
    enabled: !!id, // Prevents execution if ID is missing
  });
};

/**
 * Hook to create a new product.
 */
export const useCreateProduct = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (productData: Product) => createProductApi(productData),
    onSuccess: () => {
      toast.success("✅ Product created successfully!");
      queryClient.invalidateQueries({ queryKey: ["products"] }); // Refreshes product list
    },
    onError: () => {
      toast.error("❌ Failed to create product");
    },
  });
};

/**
 * Hook to update an existing product.
 */
export const useUpdateProduct = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({
      _id,
      productData,
    }: {
      _id: string;
      productData: Product;
    }) => updateProductApi(_id, productData),
    onSuccess: () => {
      toast.success("✅ Product updated successfully!");
      queryClient.invalidateQueries({ queryKey: ["products"] });
      queryClient.invalidateQueries({ queryKey: ["product"] });
    },
    onError: () => {
      toast.error("❌ Failed to update product");
    },
  });
};

/**
 * Hook to delete a product.
 */
export const useDeleteProduct = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) => deleteProductApi(id),
    onSuccess: () => {
      toast.success("✅ Product deleted successfully!");
      queryClient.invalidateQueries({ queryKey: ["products"] });
    },
    onError: (error: any) => {
      toast.error(`❌ Failed to delete product: ${error.message}`);
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

4. Practical Use Cases: React Components

Here are a few components that demonstrate how the hooks can be integrated into your UI. Each component is designed to be concise and focus on a single product operation.

Product List Component

// ProductList.tsx
import React from "react";
import { useGetAllProducts } from "@/hooks/use-products";

const ProductList = () => {
  const { data, isLoading, error } = useGetAllProducts();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data?.res.products.map((product: any) => (
        <li key={product._id}>
          {product.product_code} - {product.product_name}
        </li>
      ))}
    </ul>
  );
};

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

Product Details Component

// ProductDetails.tsx
import React from "react";
import { useGetSingleProduct } from "@/hooks/use-products";

const ProductDetails = ({ id }: { id: string }) => {
  const { data, isLoading, error } = useGetSingleProduct(id);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h3>Product Code: {data?.res.product.product_code}</h3>
      <p>Name: {data?.res.product.product_name}</p>
      <p>Description: {data?.res.product.description}</p>
    </div>
  );
};

export default ProductDetails;
Enter fullscreen mode Exit fullscreen mode

Create Product Component

// CreateProduct.tsx
import React, { useState } from "react";
import { useCreateProduct } from "@/hooks/use-products";
import { Product } from "@/types/product";

const CreateProduct = () => {
  const mutation = useCreateProduct();
  const [productData, setProductData] = useState<Product>({
    _id: "",
    company_user_id: "123", // Example company user ID
    product_code: "",
    product_name: "",
    description: "",
    price: 0,
    stock: 0,
    // Additional product details as needed
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    mutation.mutate(productData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Product Code"
        value={productData.product_code}
        onChange={(e) =>
          setProductData({ ...productData, product_code: e.target.value })
        }
      />
      <input
        type="text"
        placeholder="Product Name"
        value={productData.product_name}
        onChange={(e) =>
          setProductData({ ...productData, product_name: e.target.value })
        }
      />
      <button type="submit" disabled={mutation.isLoading}>
        {mutation.isLoading ? "Creating..." : "Create Product"}
      </button>
      {mutation.error && <p>Error: {mutation.error.message}</p>}
    </form>
  );
};

export default CreateProduct;
Enter fullscreen mode Exit fullscreen mode

Update Product Component

// UpdateProduct.tsx
import React, { useState } from "react";
import { useUpdateProduct } from "@/hooks/use-products";
import { Product } from "@/types/product";

const UpdateProduct = ({ id }: { id: string }) => {
  const mutation = useUpdateProduct();
  const [updatedData, setUpdatedData] = useState<Partial<Product>>({
    price: 100, // Default updated value
  });

  const handleUpdate = () => {
    mutation.mutate({ _id: id, productData: updatedData as Product });
  };

  return (
    <div>
      <button onClick={handleUpdate} disabled={mutation.isLoading}>
        {mutation.isLoading ? "Updating..." : "Update Product"}
      </button>
      {mutation.error && <p>Error: {mutation.error.message}</p>}
    </div>
  );
};

export default UpdateProduct;
Enter fullscreen mode Exit fullscreen mode

Delete Product Button (Integrated in List)

// ProductDeleteButton.tsx
import React from "react";
import { useDeleteProduct } from "@/hooks/use-products";

const ProductDeleteButton = ({ id }: { id: string }) => {
  const deleteMutation = useDeleteProduct();

  const handleDelete = (id: string) => {
    if (window.confirm("Are you sure you want to delete this product?")) {
      deleteMutation.mutate(id);
    }
  };

  return (
    <button onClick={() => handleDelete(id)} className="bg-red-500 text-white px-3 py-1 rounded">
      Delete
    </button>
  );
};

export default ProductDeleteButton;
Enter fullscreen mode Exit fullscreen mode

Strategic Considerations for C-suite Leaders

Adopting an API integration strategy that leverages Tanstack Query can yield significant business advantages:

  • Scalability & Resilience: By decoupling UI from backend services through asynchronous queries, your company can scale operations and introduce new features with minimal friction.
  • Data Integrity: Real-time synchronization ensures that product information remains accurate across all channels, aiding both internal decision-making and customer engagement.
  • Operational Efficiency: Streamlined API error handling and automated query refetching reduce downtime and free up developer resources for innovation.

Conclusion

Integrating product APIs with Tanstack Query isn’t just a technical upgrade it’s a strategic enabler for enterprise growth. With a clear setup process, robust error handling, and modular hooks for CRUD operations, your organization can deliver a more reliable, scalable, and agile digital ecosystem.

By aligning technology with business strategy, your leadership can drive competitive advantage and position your company for long-term success in a data-driven market.

Top comments (0)