Next js

Build a Full CRUD App Using Zustand + Next.js + JSON Server

🧠 What is Zustand?

Zustand (German for “state”) is a modern, minimalistic, and scalable state management library for React applications. Built by the creators of Jotai and React Three Fiber, Zustand solves the complexity of state sharing across components without context providers, reducers, or the boilerplate of Redux.

βœ… Why Choose Zustand?

  • No Provider Required: Zustand doesn’t need a <Provider> component like Redux or Context API.
  • Hook-Based API: Simply use React hooks to read and update state.
  • Slices of State: Subscribe to only the state slices you care about.
  • Middleware Support: Add devtools, persistence, or logging easily.
  • SSR Friendly: Built to work with frameworks like Next.js.

πŸ”„ Zustand’s Core Flow

Zustand works through:

  1. A global store created using the create function.
  2. React hooks that allow components to read/update the store.
  3. Selective subscriptions: Components re-render only if their slice changes.

Here’s a simple illustration:


import { create } from 'zustand';

const useCounter = create((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 }))
}));

In your component:


const { count, increase } = useCounter();

No Provider. No Boilerplate. Super intuitive.

πŸ’‘ How Zustand Works with Next.js and JSON Server

  • Zustand acts as a shared state container.
  • JSON Server acts as a fake backend.
  • All API interactions happen inside Zustand and are shared with components.
  • The entire CRUD logic is centralized in store/userStore.js.

This keeps components clean and logic encapsulated.

πŸ›  Step 1: Set Up the Project

Create your Next.js app:

zustand-crud-app/
β”œβ”€β”€ db.json                        # Fake database for JSON Server
β”œβ”€β”€ package.json                   # npm scripts (dev + server)
β”œβ”€β”€ pages/
β”‚   └── index.js                   # Main page with all components
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ Header.js                  # Shows dynamic cart count
β”‚   β”œβ”€β”€ ProductList.js             # Product catalog with "Add to Cart"
β”‚   β”œβ”€β”€ UserForm.js                # Add/Edit user form
β”‚   β”œβ”€β”€ UserList.js                # User table and delete button
β”œβ”€β”€ store/
β”‚   β”œβ”€β”€ userStore.js               # Zustand store for users
β”‚   └── shopStore.js               # Zustand store for products + cart
β”œβ”€β”€ public/
β”‚   └── favicon.ico (default)      # Static assets (optional)
β”œβ”€β”€ styles/
β”‚   └── globals.css (optional)     # Global CSS (if using Tailwind
npx create-next-app zustand-crud-app cd zustand-crud-app npm install zustand json-server --save-dev 

πŸ“ Project Structure


zustand-crud-app/
β”œβ”€β”€ db.json                  # JSON Server Fake DB
β”œβ”€β”€ store/userStore.js      # Zustand store for global state
β”œβ”€β”€ components/UserForm.js  # Add/Edit form
β”œβ”€β”€ components/UserList.js  # User listing and delete
β”œβ”€β”€ pages/index.js          # Main page
β”œβ”€β”€ package.json            # Scripts for server + dev

βš™οΈ Step 2: Configure JSON Server

1. Create a db.json file in the root:


{
  "users": [
    {
      "id": 1,
      "firstName": "John",
      "lastName": "Doe",
      "email": "john@example.com",
      "password": "123456"
    }
  ]
}

2. Add this script to package.json:


"scripts": {
  "dev": "next dev",
  "server": "json-server --watch db.json --port 5000"
}

3. Start JSON Server:


npm run server

Visit http://localhost:5000/users to access the API.

🧠 Step 3: Create the Zustand Store

File: store/userStore.js

import { create } from 'zustand';

export interface User {
  id?: number;
  firstName: string;
  lastName: string;
  email: string;
  password: string;
}

interface UserStore {
  users: User[];
  fetchUsers: () => Promise<void>;
  addUser: (user: User) => Promise<void>;
  updateUser: (id: number, updatedUser: User) => Promise<void>;
  deleteUser: (id: number) => Promise<void>;
}

const API = 'http://localhost:5000/users';

const useUserStore = create<UserStore>((set) => ({
  users: [],

  fetchUsers: async () => {
    const res = await fetch(API);
    const data = await res.json();
    set({ users: data });
  },

  addUser: async (user) => {
    const res = await fetch(API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user),
    });
    const newUser = await res.json();
    set((state) => ({ users: [...state.users, newUser] }));
  },

  updateUser: async (id, updatedUser) => {
    await fetch(`${API}/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updatedUser),
    });
    set((state) => ({
      users: state.users.map((u) =>
        u.id === id ? { ...u, ...updatedUser } : u
      ),
    }));
  },

  deleteUser: async (id) => {
    await fetch(`${API}/${id}`, { method: 'DELETE' });
    set((state) => ({
      users: state.users.filter((u) => u.id !== id),
    }));
  },
}));

export default useUserStore;

πŸ“ Step 4: Create User Form Component

File: components/UserForm.js


import { useState, useEffect } from 'react';
import useUserStore from '../store/userStore';

export default function UserForm({ editData, setEditData }) {
  const { addUser, updateUser } = useUserStore();
  const [form, setForm] = useState({ firstName: '', lastName: '', email: '', password: '' });

  useEffect(() => {
    if (editData) setForm(editData);
    else setForm({ firstName: '', lastName: '', email: '', password: '' });
  }, [editData]);

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (editData) {
      updateUser(editData.id, form);
      setEditData(null);
    } else {
      addUser(form);
    }
    setForm({ firstName: '', lastName: '', email: '', password: '' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="firstName" value={form.firstName} onChange={handleChange} />
      <input name="lastName" value={form.lastName} onChange={handleChange} />
      <input name="email" type="email" value={form.email} onChange={handleChange} />
      <input name="password" type="password" value={form.password} onChange={handleChange} />
      <button type="submit">{editData ? 'Update' : 'Add'} User</button>
    </form>
  );
}

πŸ“„ Step 5: Create User List Component

File: components/UserList.js


import { useEffect } from 'react';
import useUserStore from '../store/userStore';

export default function UserList({ setEditData }) {
  const { users, fetchUsers, deleteUser } = useUserStore();

  useEffect(() => {
    fetchUsers();
  }, []);

  return (
    <div>
      <h2>User List</h2>
      {users.length === 0 && <p>No users found.</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <div>{user.firstName} {user.lastName} - {user.email}</div>
            <button onClick={() => setEditData(user)}>Edit</button>
            <button onClick={() => deleteUser(user.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

🏠 Step 6: Build the Main Page

File: pages/index.js


import { useState } from 'react';
import UserForm from '../components/UserForm';
import UserList from '../components/UserList';

export default function Home() {
  const [editData, setEditData] = useState(null);

  return (
    <main style={{ maxWidth: '600px', margin: '2rem auto' }}>
      <h1>User CRUD with Zustand + JSON Server</h1>
      <UserForm editData={editData} setEditData={setEditData} />
      <UserList setEditData={setEditData} />
    </main>
  );
}

πŸ§ͺ Run Everything

Terminal 1: Start the Next.js app


npm run dev

Terminal 2: Start the JSON server


npm run server

Visit http://localhost:3000 to try your full-featured CRUD app.

 

🧩 Add To Cart Example

  • Add products to db.json (mock product catalog).

  • Create Zustand store to manage both products and cart.

  • Display products with an β€œAdd to Cart” button.

  • Add a <Header /> that shows total cart items dynamically.

  • Use shared global state (cart) with Zustand to sync across components.


πŸ” Step-by-Step Extension


πŸ”§ 1. Update db.json

Extend your db.json:

 { "users": [ { "id": 1, "firstName": "John", "lastName": "Doe", "email": "john@example.com", "password": "123456" } ], "products": [ { "id": 1, "name": "Laptop", "price": 999 }, { "id": 2, "name": "Headphones", "price": 199 }, { "id": 3, "name": "Keyboard", "price": 59 } ] } 

🧠 2. Update Zustand Store: store/shopStore.js

import { create } from 'zustand';

const API = 'http://localhost:5000';

const useShopStore = create((set) => ({
  products: [],
  cart: [],

  fetchProducts: async () => {
    const res = await fetch(`${API}/products`);
    const data = await res.json();
    set({ products: data });
  },

  addToCart: (product) => {
    set((state) => {
      const alreadyInCart = state.cart.find(
        (item) => item.id === product.id
      );
      if (alreadyInCart) return state; // prevent duplicates
      return { cart: [...state.cart, product] };
    });
  },

  cartCount: () => useShopStore.getState().cart.length
}));

 


🧱 3. Create components/Header.js

import useShopStore from '../store/shopStore';

export default function Header() {
  const cart = useShopStore((state) => state.cart);

  return (
    <header
      style={{
        padding: '1rem',
        background: '#f5f5f5',
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
      }}
    >
      <h2>My Shop</h2>
      <div>πŸ›’ Cart: {cart.length}</div>
    </header>
  );
}

 


πŸ›’ 4. Create components/ProductList.js

import { useEffect } from 'react';
import useShopStore from '../store/shopStore';

export default function ProductList() {
  const { products, fetchProducts, addToCart } = useShopStore();

  useEffect(() => {
    fetchProducts(); // Load products from db.json
  }, []);

  return (
    <div>
      <h3>Available Products</h3>
      <ul>
        {products.map((product) => (
          <li key={product.id} style={{ marginBottom: '10px' }}>
            <strong>{product.name}</strong> – ${product.price}
            <button
              onClick={() => addToCart(product)}
              style={{ marginLeft: '10px', padding: '4px 10px' }}
            >
              Add to Cart
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

 


🏠 5. Update pages/index.js

import { useEffect } from 'react';
import useShopStore from '../store/shopStore';

export default function ProductList() {
  const { products, fetchProducts, addToCart } = useShopStore();

  useEffect(() => {
    fetchProducts();
  }, []);

  return (
    <div>
      <h3>Products</h3>
      <ul>
        {products.map((p) => (
          <li key={p.id}>
            {p.name} - ${p.price}
            <button
              onClick={() => addToCart(p)}
              style={{ marginLeft: '10px' }}
            >
              Add to Cart
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

 


βœ… Now Your App Supports

  • Viewing products

  • Adding to cart

  • Shared cart state

  • Real-time cart count in header

  • All logic managed by Zustand

 

Leave a Reply

Your email address will not be published. Required fields are marked *