Build a CRUD Table with Django, React & Material UI: A Full-Stack Tutorial

By admin December 22, 2025 7 min read

Build a Dynamic CRUD Table with Django, React, and Material UI

Welcome back to Halogenius Ideas! Today, we’re embarking on an exciting journey to build a full-stack web application. We’ll create a sleek, functional CRUD (Create, Read, Update, Delete) interface for managing a list of items. By the end of this tutorial, you’ll have a working application with a Django backend serving a REST API and a React frontend styled with the beautiful and popular Material UI component library.

This project is perfect for beginner to intermediate developers looking to understand how separate frontend and backend systems communicate. We’ll walk through every file, explain each concept, and you’ll end up with a project structure you can expand for your own ideas. Let’s get our tools ready and start building!

To watch the full tutorial on YouTube, click here.

Key Takeaway: A CRUD application represents the four basic operations of persistent storage. Mastering this pattern is foundational to nearly all interactive web applications you will build.

Prerequisites and Project Setup

Before we dive into the code, let’s ensure we have the right environment. You’ll need Python (3.8 or higher) and Node.js (with npm) installed on your machine. We’ll be using a virtual environment for Python to manage dependencies cleanly.

First, let’s create our project directory and set up the backend (Django) and frontend (React) as two separate services. This is a modern, decoupled architecture.


# Create the main project folder
mkdir crud-table-project
cd crud-table-project

# Create and activate a Python virtual environment (Backend)
python -m venv venv
# On Windows: venv\Scripts\activate
# On Mac/Linux: source venv/bin/activate

Part 1: Building the Django Backend API

Our backend will be a Django project using the Django REST Framework (DRF) to create a robust and simple API. Think of Django as the reliable librarian who manages all our data (books) in an organized database, and DRF as the helpful assistant who neatly packages that data to send to our frontend.

Step 1: Install Dependencies and Create Project

With your virtual environment activated, install the necessary Python packages and start the Django project.


pip install django djangorestframework django-cors-headers
django-admin startproject backend
cd backend

Step 2: Create the “Item” App and Model

In Django, functionality is organized into “apps.” We’ll create an app called items to handle our data model and API logic. Our model will be simple: an Item with a name and a description.


python manage.py startapp items

Now, let’s define our model in items/models.py. This code defines the structure of our database table.


from django.db import models

class Item(models.Model):
    """
    Model representing an Item in the system.
    Fields:
    name - A short identifier for the item (CharField).
    description - A longer text describing the item (TextField).
    """
    name = models.CharField(max_length=200)
    description = models.TextField()

    def __str__(self):
        """String representation of the Item model."""
        return self.name

Step 3: Configure the Project Settings

We need to register our new app and necessary third-party apps in backend/settings.py. Crucially, we must also configure CORS (Cross-Origin Resource Sharing) to allow our React frontend (running on a different port) to communicate with the Django backend.


# backend/settings.py (Excerpts)
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Third-party apps
    'rest_framework',
    'corsheaders',
    # Local apps
    'items',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # <-- Add this at the top
    'django.middleware.security.SecurityMiddleware',
    ... # other middleware
]

# CORS Configuration
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",  # This is the default React development server port
]

# For simplicity in development, you can allow all origins (NOT for production)
# CORS_ALLOW_ALL_ORIGINS = True

Step 4: Create Serializers and Views

A serializer translates complex data (like model instances) into native Python datatypes (like dictionaries) that can be easily rendered into JSON. Our views will handle the logic for each API endpoint (GET, POST, PUT, DELETE).

First, create items/serializers.py:


from rest_framework import serializers
from .models import Item

class ItemSerializer(serializers.ModelSerializer):
    """
    Serializer for the Item model.
    Translates Item instances to/from JSON format.
    """
    class Meta:
        model = Item
        fields = ['id', 'name', 'description']  # Fields to include in the API

Next, update items/views.py to define the API logic. We'll use DRF's generic class-based views which provide common behavior.


from rest_framework import generics
from .models import Item
from .serializers import ItemSerializer

class ItemListCreate(generics.ListCreateAPIView):
    """
    API view to handle:
    GET: Retrieve a list of all items.
    POST: Create a new item.
    """
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

class ItemRetrieveUpdateDestroy(generics.RetrieveUpdateDestroyAPIView):
    """
    API view to handle a single item by its primary key (id):
    GET: Retrieve one item.
    PUT: Update an item.
    DELETE: Destroy an item.
    """
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

Step 5: Define URLs and Run Migrations

We need to wire up our views to URL endpoints. First, in items/urls.py (create this file):


from django.urls import path
from . import views

urlpatterns = [
    path('items/', views.ItemListCreate.as_view(), name='item-list-create'),
    path('items/<int:pk>/', views.ItemRetrieveUpdateDestroy.as_view(), name='item-detail'),
]

Then, include these item URLs in the main project's backend/urls.py:


from django.contrib import admin
from django.urls import path, include  # <-- Don't forget to import 'include'

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('items.urls')),  # <-- All our item APIs are under /api/
]

Finally, create the database tables by running migrations and start the Django development server:


python manage.py makemigrations
python manage.py migrate
python manage.py runserver

Tip: Your Django API is now live! You can visit http://localhost:8000/api/items/ in your browser or a tool like Postman. It should return an empty list []. Keep this server running in a terminal.

Part 2: Building the React Frontend with Material UI

Now for the user-facing part! We'll create a React application that fetches data from our Django API and presents it in a beautiful, interactive Material UI table. Open a new terminal window, navigate to your main project folder (crud-table-project), and let's begin.

Step 1: Create React App and Install Dependencies


npx create-react-app frontend
cd frontend
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material axios

We're installing the core Material UI library, its icons, and Axios—a popular HTTP client for making API requests more easily than with the native fetch API.

Step 2: The Main App Component and API Service

Let's create a clean service file to abstract all API calls. Create src/services/api.js:


import axios from 'axios';

// Create an Axios instance pointing to your Django backend
const apiClient = axios.create({
  baseURL: 'http://localhost:8000/api', // Django server address
  headers: {
    'Content-Type': 'application/json',
  },
});

// ItemService object containing all CRUD methods
const ItemService = {
  // Get all items
  getAllItems() {
    return apiClient.get('/items/');
  },
  // Get a single item by ID
  getItem(id) {
    return apiClient.get(`/items/${id}/`);
  },
  // Create a new item
  createItem(itemData) {
    return apiClient.post('/items/', itemData);
  },
  // Update an existing item
  updateItem(id, itemData) {
    return apiClient.put(`/items/${id}/`, itemData);
  },
  // Delete an item
  deleteItem(id) {
    return apiClient.delete(`/items/${id}/`);
  },
};

export default ItemService;

Now, let's build the core of our application in src/App.js. We'll break it down into sections. First, the imports and state setup:


import React, { useState, useEffect } from 'react';
import {
  Container,
  Typography,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Paper,
  Button,
  IconButton,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  TextField,
  Box,
  Snackbar,
  Alert,
} from '@mui/material';
import { Edit, Delete, Add } from '@mui/icons-material';
import ItemService from './services/api';

function App() {
  // State for the list of items
  const [items, setItems] = useState([]);
  // State for the current item being edited/created
  const [currentItem, setCurrentItem] = useState({ name: '', description: '' });
  // State to control the open/close of the form dialog
  const [openDialog, setOpenDialog] = useState(false);
  // State to determine if we are editing (true) or creating (false)
  const [isEditing, setIsEditing] = useState(false);
  // State for feedback alerts (success/error messages)
  const [alert, setAlert] = useState({ open: false, message: '', severity: 'success' });

  // Fetch items from the API when the component loads
  useEffect(() => {
    fetchItems();
  }, []);

Step 3: Implementing CRUD Functions

Inside the App function, we define the functions that will handle our operations by calling the ItemService.


  const fetchItems = async () => {
    try {
      const response = await ItemService.getAllItems();
      setItems(response.data); // Update state with fetched items
    } catch (error) {
      showAlert('Failed to fetch items.', 'error');
    }
  };

  const handleCreateOrUpdate = async () => {
    try {
      if (isEditing) {
        // Update existing item
        await ItemService.updateItem(currentItem.id, currentItem);
        showAlert('Item updated successfully!', 'success');
      } else {
        // Create new item
        await ItemService.createItem(currentItem);
        showAlert('Item created successfully!', 'success');
      }
      // Refresh the list, close the dialog, and reset the form
      fetchItems();
      handleCloseDialog();
    } catch (error) {
      showAlert(`Operation failed: ${error.message}`, 'error');
    }
  };

  const handleDelete = async (id) => {
    if (window.confirm('Are you sure you want to delete this item?')) {
      try {
        await ItemService.deleteItem(id);
        showAlert('Item deleted successfully!', 'success');
        fetchItems(); // Refresh the list
      } catch (error) {
        showAlert('Failed to delete item.', 'error');
      }
    }
  };

  const handleEditClick = (item) => {
    // Populate the form with the item's data for editing
    setCurrentItem(item);
    setIsEditing(true);
    setOpenDialog(true);
  };

  const handleCreateClick = () => {
    // Clear the form for a new item
    setCurrentItem({ name: '', description: '' });
    setIsEditing(false);
    setOpenDialog(true);
  };

  const handleCloseDialog = () => {
    setOpenDialog(false);
    // Small delay to let dialog close before resetting state (for visual smoothness)
    setTimeout(() => {
      setCurrentItem({ name: '', description: '' });
      setIsEditing(false);
    }, 100);
  };

  const showAlert = (message, severity) => {
    setAlert({ open: true, message, severity });
  };

  const handleCloseAlert = () => {
    setAlert({ ...alert, open: false });
  };

Step 4: Rendering the UI with Material UI Components

Finally, the return statement of our component renders the user interface. This is where Material UI shines, providing pre-styled, accessible components.


  return (
    <Container maxWidth="lg">
      <Box sx={{ my: 4 }}>
        <Typography variant="h3" component="h1" gutterBottom align="center">
          Item Manager
        </Typography>
        <Button
          variant="contained"
          startIcon={<Add />}
          onClick={handleCreateClick}
          sx={{ mb: 3 }}
        >
          Add New Item
        </Button>

        <TableContainer component={Paper}>
          <Table aria-label="simple table">
            <TableHead>
              <TableRow>
                <TableCell><strong>ID</strong></TableCell>
                <TableCell><strong>Name</strong></TableCell>
                <TableCell><strong>Description</strong></TableCell>
                <TableCell align="center"><strong>Actions</strong></TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {items.map((item) => (
                <TableRow key={item.id}>
                  <TableCell>{item.id}</TableCell>
                  <TableCell>{item.name}</TableCell>
                  <TableCell>{item.description}</TableCell>
                  <TableCell align="center">
                    <IconButton
                      aria-label="edit"
                      color="primary"
                      onClick={() => handleEditClick(item)}
                    >
                      <Edit />
                    </IconButton>
                    <IconButton
                      aria-label="delete"
                      color="error"
                      onClick={() => handleDelete(item.id)}
                    >
                      <Delete />
                    </IconButton>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
      </Box>

      {/* Dialog for Creating/Editing Items */}
      <Dialog open={openDialog} onClose={handleCloseDialog}>
        <DialogTitle>{isEditing ? 'Edit Item' : 'Create New Item'}</DialogTitle>
        <DialogContent>
          <TextField
            autoFocus
            margin="dense"
            label="Item Name"
            type="text"
            fullWidth
            variant="outlined"
            value={currentItem.name}
            onChange={(e) => setCurrentItem({ ...currentItem, name: e.target.value })}
            sx={{ mb: 2 }}
          />
          <TextField
            margin="dense"
            label="Description"
            type="text"
            fullWidth
            multiline
            rows={4}
            variant="outlined"
            value={currentItem.description}
            onChange={(e) => setCurrentItem({ ...currentItem, description: e.target.value })}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseDialog}>Cancel</Button>
          <Button onClick={handleCreateOrUpdate} variant="contained">
            {isEditing ? 'Update' : 'Create'}
          </Button>
        </DialogActions>
      </Dialog>

      {/* Snackbar for Alerts */}
      <Snackbar
        open={alert.open}
        autoHideDuration={6000}
        onClose={handleCloseAlert}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert onClose={handleCloseAlert} severity={alert.severity} sx={{ width: '100%' }}>
          {alert.message}
        </Alert>
      </Snackbar>
    </Container>
  );
}

export default App;

Step 5: Start the React Development Server

Now, in your frontend directory, run:


npm start

Your default browser should open to http://localhost:3000, showcasing your fully functional CRUD table!

Congratulations! You've just built a complete full-stack application. Try adding, editing, and deleting items. Notice how the frontend React state updates and synchronizes with the backend Django database via API calls. This is the core pattern of modern web development.

Conclusion and Next Steps

In this tutorial, we've successfully built a bridge between two powerful technologies: Django for a robust, secure backend API and React with Material UI for a dynamic, user-friendly frontend. You've seen how models, serializers, and views work in Django REST Framework, and how to manage state, effects, and API calls in a React functional component.

This project is a springboard. Here are some ideas to level up your skills:

  • Add Form Validation: Use Material UI's form validation or a library like Formik to ensure data quality before sending to the API.
  • Implement Search & Filter: Add a search bar above the table and create a new API endpoint in Django to handle filtered queries.
  • Enhance the Backend: Add user authentication with Django's djangorestframework-simplejwt to secure your API endpoints.
  • Deploy It: Learn to deploy the Django backend on a platform like Railway or Heroku and the React frontend on Vercel or Netlify. Remember to update the baseURL in your API service and configure production CORS settings.

To watch the full tutorial on YouTube, click here.

You can find the complete, organized code for this project in the accompanying GitHub repository. Feel free to clone it, experiment, and make it your own.

Found this guide helpful? Explore more web development and tech tutorials on Halogenius Ideas to continue your learning journey. Happy coding!

Watch The Video

Watch on YouTube

How did this article make you feel?

Share This Post

About admin

Founder and Lead Developer at Halogenius Ideas, bridging the gap between professional web design and accessible tech education. With years of experience in full-stack development and a passion for teaching, I lead a team dedicated to building stunning digital experiences while empowering the next generation of developers through comprehensive tutorials and courses. When I'm not coding or creating content, you'll find me exploring new technologies and mentoring aspiring developers.

Leave a Reply

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

four + four =
Powered by MathCaptcha