Fetching Products from React

An API Helper

Keep the API base URL in an environment variable (VITE_API_URL) and put all calls in one file:

// src/api/products.js
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';

async function request(path, options = {}) {
  const res = await fetch(API_URL + path, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });
  if (res.status === 204) return null;
  const body = await res.json();
  if (!res.ok) throw body;            // ProblemDetail from Spring: { title, detail, errors }
  return body;
}

export const getProducts = () => request('/api/products');
export const createProduct = (product) =>
  request('/api/products', { method: 'POST', body: JSON.stringify(product) });
export const deleteProduct = (id) => request('/api/products/' + id, { method: 'DELETE' });

The Product List Page

// src/pages/ProductList.jsx
import { useEffect, useState } from 'react';
import { getProducts, deleteProduct } from '../api/products';

export default function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    getProducts()
      .then(setProducts)
      .catch((e) => setError(e.detail || 'Could not load products'))
      .finally(() => setLoading(false));
  }, []);

  async function handleDelete(id) {
    await deleteProduct(id);
    setProducts((list) => list.filter((p) => p.id !== id));
  }

  if (loading) return <p>Loading...</p>;
  if (error) return <p className="error">{error}</p>;

  return (
    <table>
      <thead>
        <tr><th>Name</th><th>Category</th><th>Price</th><th>Stock</th><th></th></tr>
      </thead>
      <tbody>
        {products.map((p) => (
          <tr key={p.id}>
            <td>{p.name}</td>
            <td>{p.category}</td>
            <td>₹{p.price.toLocaleString('en-IN')}</td>
            <td>{p.inStock ? p.stock : 'Out of stock'}</td>
            <td><button onClick={() => handleDelete(p.id)}>Delete</button></td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

What the page renders with the two ShopEase products:

Name             Category      Price    Stock
Wireless Mouse   Electronics   ₹799     120            [Delete]
Clean Code       Books         ₹450     Out of stock   [Delete]

Tip: Libraries such as TanStack Query (React Query) add caching, loading states and refetching for you. axios is a popular alternative to fetch; the Spring side is the same.

Dev Proxy Instead of CORS

In development you can also avoid CORS entirely: let Vite forward /api calls to Spring, so the browser sees a single origin.

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { '/api': 'http://localhost:8080' },
  },
});