A Next.js 15 project showcasing different data fetching patterns and architectures using TypeScript, TanStack Query (React Query), Bun runtime, and Tailwind CSS v4.
This project demonstrates modern data fetching strategies in React/Next.js applications, comparing client-side approaches using native Fetch API and TanStack Query for efficient data management, caching, and mutations.
- Next.js 15.1.1 - React framework with App Router
- React 19 - Latest React version with modern features
- TypeScript - Full type safety
- TanStack Query v5 - Powerful data synchronization library
- Tailwind CSS v4 - Utility-first CSS framework
- Bun - Fast JavaScript runtime and package manager
- Radix UI - Accessible component primitives
- Lucide React - Beautiful icon library
data-fetching-architectures/
├── app/
│ ├── nativeFetch/
│ │ ├── fetcher.ts # Native fetch handler with AbortController
│ │ ├── NativeFetch.tsx # Client component with table view
│ │ └── page.tsx # Route page
│ ├── tanStackQuery/
│ │ ├── api/
│ │ │ ├── getProducts.ts # useQuery hook for fetching products
│ │ │ └── createProduct.ts # Mutation function for creating products
│ │ ├── fetcher.ts # TanStack Query fetcher
│ │ ├── ReactQuery.tsx # React Query implementation with cards
│ │ └── page.tsx # Route page
│ ├── types/
│ │ └── types.ts # Shared TypeScript types
│ ├── page.tsx # Main entry with QueryClientProvider
│ ├── layout.tsx # Root layout
│ └── globals.css # Global styles
├── components/
│ ├── ui/ # Shadcn UI components (Button, Card, Table, Input, Label)
│ ├── CustomTable.tsx # Reusable data table component
│ └── ProductCard.tsx # Product card component
├── lib/
│ └── utils.ts # Utility functions (cn, clsx)
└── public/ # Static assets
- Dual Data Fetching Patterns: Native Fetch vs TanStack Query comparison
- Request Cancellation: AbortController for preventing race conditions
- Automatic Caching: TanStack Query handles caching with configurable staleTime
- Mutations: Create products with
useMutationand automatic cache invalidation - Manual Query Triggering: Fetch data on-demand with
enabledstate control - Loading States: Comprehensive UI feedback for all states
- Error Handling: Robust error handling across both patterns
- TypeScript: End-to-end type safety with shared type definitions
- DevTools: React Query DevTools for debugging
- Multiple View Modes: Table view (native fetch) and card grid (TanStack Query)
- Responsive Design: Mobile-first responsive layouts
- Accessible Components: Built with Radix UI primitives
- Loading Indicators: Visual feedback with spinners
- Interactive Controls: Fetch, create, and delete product actions
- Form Inputs: Product creation form with ID, price, and title fields
- Bun v1.0+ installed on your system
- Node.js 20+ (optional, if not using Bun)
- Clone the repository:
git clone <repository-url>
cd data-fetching-architectures- Install dependencies:
bun install- Create a
.env.localfile in the root directory:
NEXT_PUBLIC_BASE_URL="https://fakestoreapi.com"Run the development server:
bun devOpen http://localhost:3000 to view the application.
bun run build
bun startLocation: app/nativeFetch/
A traditional client-side data fetching approach using the native Fetch API with manual state management.
Features:
- Manual state management with React hooks (
useState) - AbortController integration for request cancellation
- Explicit loading, error, and success states
- Table view with CustomTable component
- Clear/Reset functionality
Key Files:
- fetcher.ts - Async handler with state management
- NativeFetch.tsx - UI component with table
Use Cases:
- Simple, one-off requests
- Full control over fetch logic
- Learning/understanding fetch mechanics
- Scenarios where caching is not needed
Location: app/tanStackQuery/
Modern server-state management using TanStack Query v5 with automatic caching, background updates, mutations, and optimized re-rendering.
Features:
- Automatic caching with configurable
staleTime - Manual query triggering via
enabledstate - Built-in loading and error states via
useQuery - Mutations with
useMutationfor creating products - Cache invalidation with
queryClient.invalidateQueries() - DevTools for debugging
- Grid card layout with ProductCard component
Key Files:
- api/getProducts.ts -
useQueryhook wrapper for fetching products - api/createProduct.ts - POST request function for mutations
- ReactQuery.tsx - Main component with query and mutation implementation
- page.tsx - QueryClientProvider setup
Use Cases:
- Complex applications with multiple data sources
- Need for automatic caching and synchronization
- CRUD operations with cache invalidation
- Real-time data requirements
| Feature | Native Fetch | TanStack Query |
|---|---|---|
| Caching | Manual implementation | Automatic with staleTime |
| Loading States | Manual with useState | Built-in with useQuery |
| Error Handling | Manual try/catch | Built-in error states |
| Request Cancellation | Manual AbortController | Automatic |
| Background Refetching | Not available | Automatic |
| Mutations | Manual fetch POST | useMutation with callbacks |
| Cache Invalidation | Not available | queryClient.invalidateQueries |
| DevTools | Browser DevTools only | React Query DevTools |
| Code Complexity | Higher | Lower |
| Learning Curve | Lower | Higher |
| Best For | Simple requests | Complex data management |
The project uses shared TypeScript types defined in app/types/types.ts:
type Status = "ideal" | "loading" | "error" | "success";
type Product = {
id: number;
price: number;
rating: {
rate: number;
count: number;
};
title: string;
image: string;
category: string;
description: string;
};
type returnedData = Product[];
type CreateProductPayload = {
title: string;
price: number;
id: number;
};const [enabled, setEnabled] = useState(false);
const { data, isLoading, error } = GetAllProducts({
url: "/products",
staleTime: 4000, // 4 seconds
enabled, // Only fetch when enabled is true
});
// Trigger fetch on button click
<Button onClick={() => setEnabled(true)}>List All Products</Button>const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (product: CreateProductPayload) => {
return createProduct({ url: "/products", data: product });
},
onSuccess: () => {
// Invalidate and refetch products after successful creation
queryClient.invalidateQueries({ queryKey: ["products"] });
},
});
// Trigger mutation
mutation.mutate({ id: 1, price: 29.99, title: "New Product" });- Button - components/ui/button.tsx
- Card - components/ui/card.tsx
- Table - components/ui/table.tsx
- Input - components/ui/input.tsx
- Label - components/ui/label.tsx
- CustomTable - components/CustomTable.tsx - Data table for displaying products
- ProductCard - components/ProductCard.tsx - Card view for individual products
| Variable | Description | Example |
|---|---|---|
NEXT_PUBLIC_BASE_URL |
Base API URL for data fetching | https://fakestoreapi.com |
Create a .env.local file:
NEXT_PUBLIC_BASE_URL="https://fakestoreapi.com"# Development
bun dev
# Production build
bun run build
# Start production server
bun start- Native Fetch requires more boilerplate but offers complete control
- TanStack Query reduces complexity with automatic caching and state management
- AbortController is essential for preventing race conditions in manual fetch
- staleTime controls when data is considered fresh vs stale
- Cache invalidation via
queryClient.invalidateQueries()ensures UI stays in sync after mutations enabledoption allows manual control over when queries execute- Type safety improves developer experience and reduces bugs
- Shared types ensure consistency across fetch implementations
- Add mutations (POST) with TanStack Query
- Implement cache invalidation after mutations
- Add UPDATE and DELETE mutations
- Implement optimistic updates
- Add pagination support
- Implement search and filtering
- Add Server Components with streaming
- Implement error boundaries
- Add unit and integration tests
- Next.js Documentation
- TanStack Query Documentation
- Tailwind CSS v4
- Bun Documentation
- TypeScript Documentation
MIT