Build a CRUD application using Django, React & Docker - 2022
Tags: django, react, docker, python
Published: 2022-05-19 17:09:04

As a developer, CRUD operations are one of the most fundamental concepts. Today, we'll learn how to build a REST API using Django and Django Rest and a SPA with React, which we'll use to perform the CRUD operations.

## Project Setup

Firstly, we must set up the development environment. Pick up your favorite terminal and make sure you have [virtualenv](https://virtualenv.pypa.io/en/latest/) installed.
Once it’s done, create an environment and install Django and Django rest framework.

```
virtualenv --python=/usr/bin/python3.10 venv
source venv/bin/activate
pip install django django-rest-framework
```

After the installation of the packages, we can create the project and start working.

```
django-admin startproject restaurant .
```

Note: Don’t forget the dot at the end of this command. It will generate the directories and files in the current directory instead of developing them in a new directory, `restaurant`.
To ensure that the project has been well initiated, try `python manage.py runserver`. And hit `127.0.0.1:8000`.

Now let’s create a Django app.

```
python manage.py startapp menu
```

So make sure to add the `menu` app and `rest_framework` in the `INSTALLED_APPS` in `settings.py` file.

```python
#restaurant/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'menu'
    ]
```

Good. We can start working on the logic we want to achieve in this tutorial. So, we’ll write `Menu` :

- Model
- Serializer
- ViewSet
- And finally, configure routes.

### Model

The `Menu` model will only contain 5 fields.

```python
#menu/models.py
from django.db import models
    
class Menu(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    price = models.IntegerField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return self.name
```

Once it’s done, let’s create a migration and apply it.

 [Migrations](https://docs.djangoproject.com/fr/4.0/topics/migrations/)  are Django’s way of propagating changes made to the models (adding a field, deleting a field, deleting a table, creating a table, etc.) into your database.

```
python manage.py makemigrations
python manage.py migrate
```

### Serializers

 [Serializers](https://www.django-rest-framework.org/api-guide/serializers/)  allows us to convert complex Django complex data structures such as `querysets` or model instances in Python native objects that can be converted to JSON/XML format.
We’ll create a serializer to convert our data into JSON format.

```python
#menu/serializers.py

from rest_framework import serializers
from menu.models import Menu
    
class MenuSerializer(serializers.ModelSerializer):
    class Meta:
        model = Menu
        fields = ['id', 'name', 'description', 'price', 'created', 'updated']  
```

### Viewsets

 [Viewsets](https://www.django-rest-framework.org/api-guide/viewsets/) can be referred to as Controllers if you are coming from another framework. 
ViewSet is a concept developed by DRF which consists of grouping a set of views for a given model in a single Python class. 

This set of views corresponds to the predefined actions of CRUD type (Create, Read, Update, Delete), associated with HTTP methods. 

Each of these actions is a ViewSet instance method. Among these default actions, we find:

- list
- retrieve
- update
- destroy
- partial_update
- create

```python
#menu/viewsets.py
from rest_framework import viewsets
from menu.models import Menu
from menu.serializers import MenuSerializer
    
class MenuViewSet(viewsets.ModelViewSet):
    serializer_class = MenuSerializer
        
    def get_queryset(self):
        return Menu.objects.all()
```

Great. We have the logic set, but we must add the API endpoints.

First, create a file, `routers.py`.

```python

#./routers.py
from rest_framework import routers
from menu.viewsets import MenuViewSet
router = routers.SimpleRouter()
router.register(r'menu', MenuViewSet, basename='menu')


#restaurant/urls.py
from django.contrib import admin
from django.urls import path, include

from routers import router
    
urlpatterns = [
    # path('admin/', admin.site.urls),
    path('api/', include((router.urls, 'restaurant'), namespace='restaurant'))
]
```

If you haven’t started your server yet.

```
python manage.py runserver
```

Then hit `http://127.0.0.1:8000/api/menu/` in your browser.
Your browsable API is ready. 🙂 

![](https://paper-attachments.dropbox.com/s_91B4E45CE57E19C0F15A00DA56BE11B919EA6BF487475EC7F668C42504558B3E_1610292871973_Screenshot_2021-01-10+Menu+List++Django+REST+framework.png)


Let’s add CORS responses. Adding CORS headers allows other domains to access the API ressources.

```
    pip install django-cors-headers
```

Then, add it to the `INSTALLED_APPS`.

```python
# restaurant/settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]
```

You will also need to add a middleware class to listen in on responses.

```python
#restaurant/settings.py
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]
```

We'll allow requests coming from `localhost:3000` and `127.0.0.1:3000` because the frontend React server will run at these addresses.

```python
# restaurant/settings.py

# CORS HEADERS
CORS_ALLOWED_ORIGINS = [
    'http://127.0.0.1:3000',
    'http://localhost:3000'
]
```

## React.js CRUD REST API Consumption

Make sure you have the latest version of create-react-app installed.

```
yarn create-react-app restaurant-menu-front
cd restaurant-menu-front
yarn start
```

Then open [http://localhost:3000/](http://localhost:3000/) to check the running application.
We can now add the dependencies of this project.

```
yarn add axios bootstrap react-router-dom
```

With this line of command, we installed :  

- [axios](https://axios-http.com/docs/intro)  : a promised based HTTP client
- [bootstrap](https://getbootstrap.com/docs/5.1/getting-started/introduction/):  a library to prototype an app without writing too much CSS
- [react-router-dom](https://reactrouter.com/web/guides/quick-start) : a React library for routes in our application.

Inside the `src/` folder, ensure you have the following files and directories. 

![Directory image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lufaz1kiwh7724enzz7u.png)

In  the `src/components/` directory, we have three components :  

- `AddMenu.js`
- `UpdateMenu.js`
- `MenuList.js`

And in `src/services/` directory, create `menu.service.js` and the following lines :  

```javascript
    export const baseURL = "http://localhost:8000/api";
    export const headers = {
      "Content-type": "application/json",
    };
```

Make sure to import  `react-router-dom` in your `index.js` file and wrap `App` in `BrowserRouter` object.

```javascript
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
```

Once it’s done, we can change the `App.js` file by importing `bootstrap`, writing routes, and building the home page and the navigation bar.

```javascript
import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import { Routes, Route, Link } from "react-router-dom";

import { AddMenu } from "./components/AddMenu";
import { MenuList } from "./components/MenuList";
import { UpdateMenu } from "./components/UpdateMenu";

function App() {
  return (
    <div>
      <nav className="navbar navbar-expand navbar-dark bg-info">
        <a href="/" className="navbar-brand">
          Restaurant Menu
        </a>
        <div className="navbar-nav mr-auto">
          <li className="nav-item">
            <Link to={"/add/"} className="nav-link">
              Add a menu
            </Link>
          </li>
        </div>
      </nav>

      <div className="container m-10">
// Adding the routes       
      </div>
    </div>
  );
}

export default App;
```

We’ll need to write the routes that should map to a component we created.

```javascript
<div className="container m-10">
  <Routes>
    <Route path="/" element={<MenuList />} />
    <Route path="/add/" element={<AddMenu />} />
    <Route path="/menu/:id/update/" element={<UpdateMenu />} />
  </Routes>
</div>
```

The next step is to write the CRUD logic and the HTML for our components.
Let’s start by listing the menu from the API in `MenuList.js`.

For this script, we’ll have two states :  

- `menus` which will store the response object from the API
- `deleted` that will contain a Boolean object to show a message

And three methods :

- `retrieveAllMenus()` to retrieve all menus from the API and set the response objects in menus using `setMenus` .
- `deleteMenu()`to delete a menu and set the `deleted` state to `true`, which will help us show a simple message every time a menu is deleted.
- `handleUpdateClick()` to navigate to a new page to update a menu.

```javascript
import axios from "axios";
import React, { useState, useEffect } from "react";
import { baseURL, headers } from "./../services/menu.service";
import { useNavigate } from "react-router-dom";

export const MenuList = () => {
  const [menus, setMenus] = useState([]);
  const navigate = useNavigate();

  const [deleted, setDeleted] = useState(false);

  const retrieveAllMenus = () => {
    axios
      .get(`${baseURL}/menu/`, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setMenus(response.data);
        console.log(menus);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const deleteMenu = (id) => {
    axios
      .delete(`${baseURL}/menu/${id}/`, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setDeleted(true);
        retrieveAllMenus();
      })
      .catch((e) => {
        console.error(e);
      });
  };


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

  const handleUpdateClick = (id) => {
    navigate(`/menu/${id}/update/`);
  };
    return (
        // ...
      );
    };
```

Once it’s done, let’s put in place the `return()` method:

```html
    <div className="row justify-content-center">
          <div className="col">
            {deleted && (
              <div
                className="alert alert-danger alert-dismissible fade show"
                role="alert"
              >
                Menu deleted!
                <button
                  type="button"
                  className="close"
                  data-dismiss="alert"
                  aria-label="Close"
                >
                  <span aria-hidden="true">&times;</span>
                </button>
              </div>
            )}
            {menus &&
              menus.map((menu, index) => (
                <div className="card my-3 w-25 mx-auto">
                  <div className="card-body">
                    <h2 className="card-title font-weight-bold">{menu.name}</h2>
                    <h4 className="card-subtitle mb-2">{menu.price}</h4>
                    <p className="card-text">{menu.description}</p>
                  </div>
                  <div classNameName="card-footer">
                    <div
                      className="btn-group justify-content-around w-75 mb-1 "
                      data-toggle="buttons"
                    >
                      <span>
                        <button
                          className="btn btn-info"
                          onClick={() => handleUpdateClick(menu.id)}
                        >
                          Update
                        </button>
                      </span>
                      <span>
                        <button
                          className="btn btn-danger"
                          onClick={() => deleteMenu(menu.id)}
                        >
                          Delete
                        </button>
                      </span>
                    </div>
                  </div>
                </div>
              ))}
          </div>
        </div>
```

![](https://paper-attachments.dropbox.com/s_91B4E45CE57E19C0F15A00DA56BE11B919EA6BF487475EC7F668C42504558B3E_1613141850249_Screenshot_2021-02-12+React+App.png)

### Add a menu
The `AddMenu.js` component has a Form to submit a new menu. It contains three fields :  `name`, `description` & `price` .

```javascript
import axios from "axios";
import React, { useState } from "react";
import { baseURL, headers } from "./../services/menu.service";

export const AddMenu = () => {
  const initialMenuState = {
    id: null,
    name: "",
    description: "",
    price: 0,
  };

  const [menu, setMenu] = useState(initialMenuState);
  const [submitted, setSubmitted] = useState(false);

  const handleMenuChange = (e) => {
    const { name, value } = e.target;
    setMenu({ ...menu, [name]: value });
  };

  const submitMenu = () => {
    let data = {
      name: menu.name,
      description: menu.description,
      price: menu.price,
    };

    axios
      .post(`${baseURL}/menu/`, data, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setMenu({
          id: response.data.id,
          name: response.data.name,
          description: response.data.description,
          price: response.data.price,
        });
        setSubmitted(true);
        console.log(response.data);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const newMenu = () => {
    setMenu(initialMenuState);
    setSubmitted(false);
  };
    return (
        // ...
      );
    };
```

For this script, we’ll have two states :  

- `menu`, which will contain by default the value of `initialMenuState` object
- `submitted` will contain a Boolean object to show a message when a menu is added.

And three methods :

- `handleInputChange()` to track the input value and set the state for change.
- `saveMenu()`to send a `POST` request to the API.
- `newMenu()` allows the user to add a new menu again once the success message has been shown.

```html
<div className="submit-form">
  {submitted ? (
    <div>
      <div
        className="alert alert-success alert-dismissible fade show"
        role="alert"
      >
        Menu Added!
        <button
          type="button"
          className="close"
          data-dismiss="alert"
          aria-label="Close"
        >
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <button className="btn btn-success" onClick={newMenu}>
        Add
      </button>
    </div>
  ) : (
    <div>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          className="form-control"
          id="name"
          required
          value={menu.name}
          onChange={handleMenuChange}
          name="name"
        />
      </div>

      <div className="form-group">
        <label htmlFor="description">Description</label>
        <input
          type="text"
          className="form-control"
          id="description"
          required
          value={menu.description}
          onChange={handleMenuChange}
          name="description"
        />
      </div>

      <div className="form-group">
        <label htmlFor="price">Price</label>
        <input
          type="number"
          className="form-control"
          id="price"
          required
          value={menu.price}
          onChange={handleMenuChange}
          name="price"
        />
      </div>

      <button
        type="submit"
        onClick={submitMenu}
        className="btn btn-success mt-2"
      >
        Submit
      </button>
    </div>
  )}
</div>
```

### Update a Menu

The component will be a little bit identical to `AddMenu` component. But, it will contain a get method to retrieve the object's current value by making a `GET` request to the API with the `id` of the object.
We use the `useHistory()` hook to pass the `id` to the `UpdateMenu` component and retrieve it with `useParams` hook.

```javascript
import axios from "axios";
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { baseURL, headers } from "./../services/menu.service";

export const UpdateMenu = () => {
  const initialMenuState = {
    id: null,
    name: "",
    description: "",
    price: 0,
  };

  const { id } = useParams();

  const [currentMenu, setCurrentMenu] = useState(initialMenuState);
  const [submitted, setSubmitted] = useState(false);

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

  const handleMenuChange = (e) => {
    const { name, value } = e.target;
    setCurrentMenu({ ...currentMenu, [name]: value });
  };

  const retrieveMenu = () => {
    axios
      .get(`${baseURL}/menu/${id}/`, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setCurrentMenu({
          id: response.data.id,
          name: response.data.name,
          description: response.data.description,
          price: response.data.price,
        });
        console.log(currentMenu);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const updateMenu = () => {
    let data = {
      name: currentMenu.name,
      description: currentMenu.description,
      price: currentMenu.price,
    };

    axios
      .put(`${baseURL}/menu/${id}/`, data, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setCurrentMenu({
          id: response.data.id,
          name: response.data.name,
          description: response.data.description,
          price: response.data.price,
        });
        setSubmitted(true);
        console.log(response.data);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const newMenu = () => {
    setCurrentMenu(initialMenuState);
    setSubmitted(false);
  };
  return (
          // ...
      );
    };
```

And this is the code inside the `return` :

```html
<div className="submit-form">
  {submitted ? (
    <div>
      <div
        className="alert alert-success alert-dismissible fade show"
        role="alert"
      >
        Menu Updated!
        <button
          type="button"
          className="close"
          data-dismiss="alert"
          aria-label="Close"
        >
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <button className="btn btn-success" onClick={newMenu}>
        Update
      </button>
    </div>
  ) : (
    <div>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          className="form-control"
          id="name"
          required
          value={currentMenu.name}
          onChange={handleMenuChange}
          name="name"
        />
      </div>

      <div className="form-group">
        <label htmlFor="description">Description</label>
        <input
          type="text"
          className="form-control"
          id="description"
          required
          value={currentMenu.description}
          onChange={handleMenuChange}
          name="description"
          default
        />
      </div>

      <div className="form-group">
        <label htmlFor="price">Price</label>
        <input
          type="number"
          className="form-control"
          id="price"
          required
          value={currentMenu.price}
          onChange={handleMenuChange}
          name="price"
        />
      </div>

      <button onClick={updateMenu} className="btn btn-success">
        Submit
      </button>
    </div>
  )}
</div>
```
And we are set now.

If you click on `Update` button on a menu card, you’ll be redirected to a new page, with this component, with the default values in the fields.
![Check the demo](https://www.dropbox.com/s/wu4pdt0hyf7kae6/road.mp4?dl=0)

## Docker build (Optional)

## Docker + Docker Compose (Optional)

[Docker](https://www.docker.com/)  is an open platform for developing, shipping, and running applications inside containers. 

Why use Docker? 
It helps you separate your applications from your infrastructure and helps in delivering code faster. 

If it's your first time working with Docker, I highly recommend you go through a quick tutorial and read some documentation about it. 

Here are some great resources that helped me: 
-  [Docker Tutorial](https://www.youtube.com/watch?v=eN_O4zd4D9o&list=PLPoSdR46FgI5wOJuzcPQCNqS37t39zKkg) 
-  [Docker curriculum](https://docker-curriculum.com/) 

### Docker configuration for the API

The `Dockerfile` represents a text document containing all the commands that could call on the command line to create an image.

Add a Dockerfile at the root of the Django project: 

```docker
# pull official base image
FROM python:3.10-alpine

# set work directory
WORKDIR /app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
 && apk add gcc python3-dev

# install python dependencies
COPY requirements.txt /app/requirements.txt
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt


# copy project
COPY . .
```

Here, we started with an **Alpine-based Docker Image for Python**. It's a lightweight Linux distribution designed for security and resource efficiency. 
After that, we set a working directory followed by two environment variables: 
 
1 - `PYTHONDONTWRITEBYTECODE` to prevent Python from writing `.pyc` files to disc
2 - `PYTHONUNBUFFERED` to prevent Python from buffering `stdout` and `stderr`

After that, we perform operations like: 
- Setting up environment variables
- Installing the PostgreSQL server package
- Copying their `requirements.txt` file to the app path, upgrading pip, and installing the python package to run our application
- And last copying the entire project

Also, let's add a `.dockerignore` file. 

```shell
env
venv
Dockerfile
```

### Docker Compose for the API

[Docker Compose](https://docs.docker.com/compose/)  is a great tool (<3). You can use it to define and run multi-container Docker applications.
 
What do we need? Well, just a YAML file containing all the configuration of our application's services. 
Then, with the `docker-compose` command, we can create and start all those services.

This file will be used for development.

```yaml
version: '3.9'
services:
 api:
 container_name: menu_api
 build: .
 restart: always
 env_file: .env
 ports:
      - "8000:8000"
 command: >
      sh -c " python manage.py migrate &&
          gunicorn restaurant.wsgi:application --bind 0.0.0.0:8000"
 volumes:
     - .:/app
```

Let's add `gunicorn` and some configurations before building our image.
```
pip install gunicorn
```

And add it as a requirement as well in the `requirements.txt`. 
Here's what my `requirements.txt` file looks like : 

```txt
django==4.0.4
django-cors-headers==3.12.0
djangorestframework==3.13.1
gunicorn==20.1.0
```


The setup is completed. Let's build our containers and test if everything works locally.

```
docker-compose up -d --build
```

Your project will be running on `https://localhost:8000/`.

### Dockerfile for the React App

Add a Dockerfile at the root of the React project: 

```docker
FROM node:17-alpine

WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .
```

Here, we started with an **Alpine-based Docker Image for JavaScript**. It's a lightweight Linux distribution designed for security and resource efficiency.

Also, let's add a `.dockerignore` file.

```
node_modules
npm-debug.log
Dockerfile
yarn-error.log
```

And let's add the code for the `docker-compose.yaml`. 

```YAML
version: "3.9"

services:
 react-app:
 container_name: react_app
 restart: on-failure
 build: .
 volumes:
      - ./src:/app/src
 ports:
      - "3000:3000"
 command: >
      sh -c "yarn start"
```

The setup is completed. Let's build our containers and test if everything works locally.

```
docker-compose up -d --build 
```

Your project will be running on `https://localhost:3000/`. And voilà! We've dockerized the API and the React applications.🚀


## Conclusion

In this article, We learned to build a CRUD application web with Django and React. And as every article can be made better, your suggestion or questions are welcome in the comment section. 😉
Check the code of all these articles in this [repo](https://github.com/Kolawole39/restaurant-menu-tutorial).

*This article has been originally posted on my [blog](https://koladev.xyz/blog/django-react-crud/)*