Escape Tutorial Hell: Master React + TanStack Query CRUD in Under 10 Minutes!

Omkar GawdeOmkar Gawde
4 min read

Hey devs! Today we're diving into how to set up a read operation in a React app using TanStack Query with an Express backend and Prisma ORM. I know a ton of you are jumping on the Next.js bandwagon (for good reason!), but there's still plenty of great use cases for a classic React + Express setup.

This guide is the perfect antidote to tutorial hell—giving you just enough practical code to start building immediately, rather than endlessly consuming content. Whether you're looking to get your hands dirty with a real implementation or simply need a quick refresher on React + TanStack Query patterns, this blog cuts through the noise to get you coding productively in minutes.


💡
Note: To fully understand this blog, you should have a basic understanding of JavaScript, React (hooks & components), and Prisma ORM, but don't worry if you're not an expert, as we'll walk through the essential concepts step by step.

WHY USE REACT QUERY INSTEAD OF NEXTJS?

While Next.js is awesome, here are three advantages of sticking with React + TanStack Query:

  1. Simpler Mental Model: Sometimes you just don't need all the extra features and complexity that Next.js brings. React + TanStack Query can be easier to reason about for smaller projects.

  2. Full Control Over Backend: When using Express directly (instead of API routes), you have full control over your server configuration, middleware stack, and scaling options.

  3. Gradual Migration Path: If you've got an existing React app, adding TanStack Query is much easier than migrating the whole thing to Next.js. It's a great way to modernize without completely rewriting.

Let's get straight into the code!

Prisma Schema: Defining Our Data

First, let's set up a simple User model in Prisma:

// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}

Express Backend: Setting Up the API

Create a route to fetch all users:

// backend/routes/users.js
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const router = express.Router();
const prisma = new PrismaClient();

// GET all users
router.get('/', async (req, res) => {
  try {
    const users = await prisma.user.findMany();
    res.json(users);
  } catch (error) {
    console.error('Error fetching users:', error);
    res.status(500).json({ error: 'Failed to fetch users' });
  }
});

module.exports = router;

Connect it to your Express app:

// backend/app.js
const express = require('express');
const cors = require('cors');
const usersRouter = require('./routes/users');

const app = express();
app.use(cors());
app.use(express.json());

app.use('/api/users', usersRouter);

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

React Frontend: Setting Up TanStack Query

Set up the QueryClient provider:

// frontend/src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

Create a custom hook for fetching users:

// frontend/src/hooks/useUsers.js
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const API_URL = 'http://localhost:3001/api';

// Function to fetch all users
const fetchUsers = async () => {
  const response = await axios.get(`${API_URL}/users`);
  return response.data;
};

// Custom hook for fetching users
export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });
}

Create a react component for UserList

// frontend/src/components/UserList.jsx
import React from 'react';
import { useUsers } from '../hooks/useUsers';

function UserList() {
  const { data: users, isLoading, error } = useUsers();

  if (isLoading) return <div>Loading users...</div>;

  if (error) return <div>Error loading users: {error.message}</div>;

  return (
    <div>
      <h2>Users</h2>
      {users?.length === 0 ? (
        <p>No users found</p>
      ) : (
        <ul>
          {users?.map(user => (
            <li key={user.id}>
              <strong>{user.name}</strong> ({user.email})
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default UserList;

Finally, use the component in your App:

// frontend/src/App.jsx
import React from 'react';
import UserList from './components/UserList';

function App() {
  return (
    <div className="App">
      <h1>My User Management App</h1>
      <UserList />
    </div>
  );
}

export default App;

In the similar fashion we can perform CRUD operations as well, and mutation is a real game changer when working with TanStack Query because it provides elegant solutions for optimistic updates, automatic cache invalidation, and retry logic—all while maintaining a clean separation between API calls and UI components.

What's Next?

In our next blog post, we'll dive into mutations with TanStack Query - showing you how to create, update, and delete those users with all the cool optimistic updates and invalidation tricks that make TanStack Query so powerful. Stay tuned!

Drop a comment if you'd like to see anything specific in the mutation tutorial, and don't forget to share this with your fellow React devs!

0
Subscribe to my newsletter

Read articles from Omkar Gawde directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Omkar Gawde
Omkar Gawde