Building a FullStack Application with Django, Django REST & Next.js

Building a FullStack Application with Django, Django REST & Next.js

ยท

24 min read

Django and Nextjs are the most used web frameworks for backend and frontend development. Django comes with a robust set of features, security, and flexibility that allows any developer to build simple yet complex applications in a few times. On the other hand, Nextjs is absolutely the preferred framework when it comes to developing a reactive frontend with React.

Django, a Python-based framework, is known for its "batteries-included" approach. It simplifies backend development so you can focus more on writing your app without needing to reinvent the wheel. On the other hand, Next.js elevates React-based frontends, offering features like server-side rendering for faster load times and better SEO. Together, they form a powerful duo for full-stack development.

In this article, we will learn how to build a fullstack application using Django as the backend to build a REST API and then create a nice and simple frontend with Nextjs to consume the data.

The application is a simple CRUD app to manage a restaurant's menu. From the frontend, the user should be able to:

  • list all menus

  • retrieve a menu

  • create a menu

  • update a menu

  • delete a menu

At the end of this article, you will understand how to connect a Django application and a frontend application built with Nextjs.

Setup

To start, let's set up our project with the necessary tools and technologies. We'll be using Python 3.11 and Django 4.2 for the backend of our application. These up-to-date versions will help ensure that our backend runs smoothly and securely.

For the frontend, we'll use Next.js 13 and Node 19.

In terms of styling, weโ€™ll use Tailwind CSS.

Building the Django API

Using Django only, it is quite impossible to build a robust API. You can indeed return JSON data from the views function or classes of your application, but how do you deal with permissions, authentications, parsing, throttling, data serialization, and much more?

That is where the Django REST framework comes into play. It is a framework developed with the architecture of Django to help developers build powerful and robust REST APIs.

Without too much hesitation, let's create the Django application.

Creating the application

Ensure that Python 3.11 is installed. In the terminal of your machine, run the following commands to create a working directory, the virtual environment, and then the project.

mkdir menu-api && cd menu-api
python3.11 -m venv venv

source venv/bin/activate

pip install django djangorestframework

django-admin startproject RestaurantCore .

Above, we just created a new Django project called RestaurantCore. You will notice a new directory and files in your current working directory.

The RestaurantCore contains files such as :

  • settings.py that contains all configurations of the Django project. This is where we will add configurations for the Django rest-framework package and other packages.

  • urls.py that contains all the URLs of the project.

  • wsgi which is useful for running your Django application in development mode and also for deployment.

To allow the admin to make CRUD operations on menu objects, we need to add an application that will contain all the logic required to treat a request, serialize or deserialize the data, and finally save it in the database.

We are reusing the MVT architecture ( Model - View - Template ), but we are replacing the layers of views and templates with serializers and viewsets.

So let's start with adding the application.

Adding the Menu application

In the current working directory, type the following command to create a new Django application.

django-admin startapp menu

Once you have executed this command, ensure to have the same structure of the directory as in the following image :

After creating the Django application, we need to register the newly created application in the INSTALLED_APPS of the settings.py file of the Django project. We will also register the rest_framework application for the rest-framework package to actually work.

# RestaurantCore/settings.py
...
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    #third apps
    "rest_framework",

    # installed apps

    "menu"
]

With the application added to the INSTALLED_APPS list, we can now start writing the menu Django application logic.

Let's start with the models.py file. Most of the time, this file contains a model which is a representation of a table in the database. Using the Django ORM, we do not need to write a single line of SQL to create a table and add fields.

# menu/models.py

from django.db import models


class Menu(models.Model):
    name = models.CharField(max_length=255)
    price = models.FloatField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

The Menu model comes with fields such as :

  • name for the name of the menu

  • price for the price

  • created for the date of creation of the object. With auto_now_add set to True, the data is automatically added at the creation.

  • And finally updated for the date of update or modification of the object. With auto_now each time the object is saved, the date is updated.

The next step is to add a serializer. This will help Django convert JSON data to Python native objects seamlessly which can be dealt with more ease.

In the menu application, in the serializers.py file, add the following content

from rest_framework import serializers

from menu.models import Menu


class MenuSerializer(serializers.ModelSerializer):

    class Meta:
        model = Menu
        fields = ['name', 'price', 'created', 'updated', 'id']
        read_only_fields = ['created', 'updated', 'id']

In the lines above, we are creating a serializer using the ModelSerializer class. The ModelSerializer class is a shortcut for adding a serializer for a model to deal with querysets and field validations seamlessly, thus no need to add our validation logic and error handling.

In this serializer, we are defining a Meta class and we set the models, the fields but also the read-only fields. These fields should not be modified with mutation requests for example.

Great! Now that we have a model serializer, then let's add the viewset to define the interface ( controller ) that will handle the requests. Create in the menu application folder, a file called viewsets and add the following.

from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from menu.models import Menu

from menu.serializers import MenuSerializer


class MenuViewSet(viewsets.ModelViewSet):
    queryset = Menu.objects.all()
    serializer_class = MenuSerializer
    permission_classes = [AllowAny]

In the code above, we are creating a new viewset class with the ModelViewSet class. Why use a viewset and not an API view? Well, viewsets already come with all the needed logic for CRUD operations such as listing, retrieving, updating, creating, and deleting. This helps us make the dev process faster and cleaner.

For this endpoint, we want to allow any users to make these CRUD operations. ( We will deal with authentication and permissions in the next article ๐Ÿ˜ )

We have the viewset that can help with CRUD operations, we need to register the viewsets and expose the API endpoint to manage menus.

Adding the Menu Endpoint

In the root directory of the Django project, create a file called routers.py . This file will contain all the endpoints of the API, in this case, the /menu/ endpoint. Then, we will register these endpoints on the urls.py file of the Django application.

Let's start by writing the code for the routers.py file.

# ./routers.py

from rest_framework import routers

from menu.viewsets import MenuViewSet

router = routers.SimpleRouter()

router.register(r'menu', MenuViewSet, basename="menu")

urlpatterns = router.urls

In the code above, here is what's happening:

  • The routers.SimpleRouter() is used to create a simple default router that automatically generates URLs for a DRF ViewSet.

  • MenuViewSet from menu.viewsets is registered with the router.

  • The basename parameter in router.register is set to "menu". This basename is used to construct the URL names for the MenuViewSet.

  • Finally, urlpatterns = router.urls sets the urlpatterns for this part of the application to those generated by the router for the MenuViewSet.

We can now register the defined router URLs in the urls.py file of the Django project.

# RestaurantCore/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, 'core_api'), namespace='core_api')),
]

In the code above, we are registering the new url in the Django application. The line path('api/', include((router.urls, 'core_api'), namespace='core_api')) defines a path that includes all URLs from the router, prefixed with 'api/'. This is nested within a namespace 'core_api'.

With the URLs and endpoint defined, we should be able to move to the creation of the frontend and start consuming data from the API ๐Ÿ˜. But wait, there is still something we need to configure, web developer's biggest opps : CORS.

Important configuration: CORS

Before the API is usable from a frontend POV, we need to configure CORS. But what are CORS? Cross-Origin Resource Sharing (CORS) is a security feature implemented in web browsers to control how web pages in one domain can request resources from another domain. It's an important part of web security because it helps prevent malicious attacks, such as Cross-Site Scripting (XSS) and data theft, by restricting how resources can be shared between different origins.

In our case, making a request from the browser to the API URL will return a frontend error, a very ugly and sometimes frustrating one.

Cors Errors

Let's solve this error by configuring CORS on the API we have created. First, let's install the django-cors-headers package.

python -m pip install django-cors-headers

Once the installation is done, open the settings.py file and ensure to add the following configurations.

INSTALLED_APPS = [
    ...,
    "corsheaders",
    ...,
]

MIDDLEWARE = [
    ...,
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    ...,
]

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
]

In the code above, the CORS_ALLOWED_ORIGINS helps us tell Django which domain origin to accept. As we are planning to use Next.js on the frontend and these apps run by default on the 3000 port, we are adding two addressees from which Django can let requests coming from.

Great! We have successfully built a Django REST API, ready to serve data. With the api/menu endpoint, we can list menus, and create a menu and by using the detail endpoint api/menu/<menu_id>, we can update a menu or delete one.

In the next section, we will build a Next.js frontend using the App router architecture that will consume data from the Django backend we have created.

Building the frontend with Next.js

In the precedent section of this article, we have built the backend of our full-stack application using Django. In this section, we will build the front end by using Next.js, a React framework built to make the development and deployment of a React application much easier than using the library directly.

We will build the UI of the frontend application just by using CSS. We will start with the listing, the page for creation, and the page for editing an article. Without further due, let's start by creating the Next.js project.

Setup the Next.js project

The Next.js team has made the creation of a Next.js project quite easy. Run the following command to create a new project.

npx create-next-app@latest

You will be presented with options to choose for the configuration of the project. Here are the options to follow if you want to configure the project following this article.

What is your project named? next-js-front
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No

The most interesting option I believe here is the choice of

Once you are done choosing the options for the creation of the project, a new directory called next-js-front containing all the resources needed to develop, start, and build the Next.js project will be created.

cd next-js-front && npm run dev

This will start the project on localhost:3000.

With the project installed, we can now move to building the first block of the application.

Building the listing page for the articles

In this section, we will build a listing page for the articles. On the page, the user should be able to view a list of articles, and a button to add a new menu, and for each item (menu) shown in the list, there should be actions for editing and deletion.

At the end of the article, it should look like in the image below.๐Ÿ‘‡

Now that we have an idea about how the interface should look like, let's start coding.

In the Next.js project, you will find the src/app content of the Next.js project. The app folder should contain files such as page.js, layout.js and style.css. Here is a quick description of each file and its goal.

  • page.js : Next.js version 13 introduced the AppRouter architectural pattern, marking a shift from the previous versions' approach of structuring files in a pages directory. This new pattern enhances routing clarity and simplicity by determining the structure of the frontend page rendering based on the file and directory organization.

    The AppRouter brings notable improvements. It's not just faster; it also features server-side rendering as a default, facilitating the use of server components. Additionally, it extends functionality with features like layout.js, which I will explain later. It also includes specific files for various functions, such as error.js for error handling and loading.js default loading behaviors.

    In this framework, creating a route like menu/supplements on the client side in a Next.js application involves placing a page.js file in the corresponding menu/supplements directory. This focus on page.js files simplify the structure, as other files in the directory are not involved in routing. This approach grants developers more flexibility in organizing their application's architecture, allowing for the placement of components used on a specific page adjacent to the page's declaration.

  • layout.js : In Next.js, particularly from version 13 with the AppRouter, the layout.js file is integral to defining the application's overall layout and style. It sets up a consistent framework across your app, encompassing elements like headers, footers, and navigation bars. layout.js supports hierarchical layouts, meaning different sections of your app can have unique layouts by having their own layout.js files. It also allows for dynamic layouts, adapting to different pages or application states.

    This file is key for integrating components like global state management and theme providers, ensuring a consistent environment across all pages. Additionally, layout.js is beneficial for SEO and performance optimization, as it centralizes metadata management and reduces the need for re-rendering common elements.

  • style.css : Well, it contains the CSS code of the project. We will inject it into the layout.js file.

Let's start coding now. We will start by adding the necessary css class definitions so we can focus on the Next.js code.

// src/app/style.css

.menu-container {
    max-width: 70%;
    height: 90vh;
    margin: 0 auto;
    padding: 20px;
    background: #f9f9f9;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.menu-item {
    border: 1px solid #ddd;
    padding: 10px;
    margin: 10px;
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #fff;
}

.menu-item-info {
    display: flex;
    flex-direction: column;
}

.menu-item-name {
    font-size: 1.2rem;
    font-weight: 600;
}

.menu-item-price {
    color: #555;
}

.menu-item-actions {
    display: flex;
    gap: 10px;
}

button {
    padding: 5px 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.edit-button {
    background-color: #ffca28;
    color: #333;
}

.add-button {
    background-color: #008000;
    color: #fff;
    padding: 10px;
    margin: 10px;
}

.delete-button {
    background-color: #f44336;
    color: #fff;
}

form {

    width: 60%;
}

.form-item {
    padding: 10px;
    display: flex;
    flex-direction: column;
}

input {
    height: 22px;
    border-radius: 4px;
    border: solid black 0.5px;
}

.success-message {
    color: #008000;
}

.error-message {
    color: #f44336;
}

And then in the src/app/layout.js, let's import the CSS code but also modify the container className to menu-container.

// src/app/layout.js

import { Inter } from "next/font/google";

import "./style.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
  title: "Restaurant Menu",
  description: "A simple UI to handle menus",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <main className="menu-container">{children}</main>
      </body>
    </html>
  );
}

In the code above, we are importing fonts and CSS files, mostly assets. When we declare the metadata object with the title and the description. Then we define the RootLayout component with menu-containerclassName, and also importing our font into the body tag.

Safe to say that we have a complete layout.js now, and we can move to writing the page.js code. It will contain the code for listing the menus. So navigating / should send you to the listing of all menus.

Building the Listing Page

In the precedent sections, we have ensured to have the necessary code for the CSS and also defined the layout.js file. We can now build the interface for the listing page.

In the src/app/page.js, make sure to have the following imports in the file to start.

// src/app/page.js

"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";

You might have noticed the use client directive. This tells Next.js to render this page on the client side, so the page we are building should contain client-side code.

If you want server-side code on this page, use the directive use server instead.

Apart from that, we are importing the useState and useEffect hooks to respectively manage states and effects in the application. The useRouter and useSearchParams will also be useful as we have buttons to redirect to the editing page and adding page.

Next, we will declare two functions, one to retrieve the list of menus from the API and the other one to delete a menu.

// src/app/page.js

...
/**
 * Fetches a menu item by ID.
 * @param {number} id The ID of the menu item to retrieve.
 */
async function deleteMenu(id) {
  const res = await fetch(`http://127.0.0.1:8000/api/menu/${id}/`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error("Failed to retrieve menu");
  }
  return Promise.resolve();
}

/**
 * Fetches menu data from the server.
 */
async function getData() {
  const res = await fetch("http://127.0.0.1:8000/api/menu/");
  if (!res.ok) {
    throw new Error("Failed to fetch data");
  }
  return res.json();
}

In the code above, we are declaring two functions :

  • deleteMenu : which takes one parameter, the id of the article, and then sends a deletion request. We are using the fetch API to send requests.

  • getData : which requests to retrieve all menus from the API. We return the json of the response.

We have the methods that we will use in the listing of articles. We need now to write the item component that will be used to display information about a menu in the list of menus.

In the same page.js file, add the following MenuItem component.

...
/**
 * Represents a single menu item.
 */
const MenuItem = ({ id, name, price, onEdit, onDelete }) => {
  return (
    <div className="menu-item" data-id={id}>
      <div className="menu-item-info">
        <div className="menu-item-name">{name}</div>
        <div className="menu-item-price">${price.toFixed(2)}</div>
      </div>
      <div className="menu-item-actions">
        <button className="edit-button" onClick={onEdit}>
          Edit
        </button>
        <button
          className="delete-button"
          onClick={() => {
            deleteMenu(id).then(() => onDelete(id));
          }}
        >
          Delete
        </button>
      </div>
    </div>
  );
};

In the code above, we are defining the MenuItem component taking props such as the id of the menu, the name, the price, the onEdit method of how to behave when the Edit button is clicked, and finally the onDelete method that is triggered when the delete button is created.
We can now move on to writing the code for the page and using the MenuItem component.

...
/**
 * The main page component.
 */
export default function Page() {
  const [menuItems, setMenuItems] = useState(null);
  const router = useRouter();
  const params = useSearchParams();

  // State for displaying a success message
  const [displaySuccessMessage, setDisplaySuccessMessage] = useState({
    show: false,
    type: "", // either 'add' or 'update'
  });

  // Fetch menu items on component mount
  useEffect(() => {
    const fetchData = async () => {
      const data = await getData();
      setMenuItems(data);
    };
    fetchData().catch(console.error);
  }, []);

  // Detect changes in URL parameters for success messages
  useEffect(() => {
    if (!!params.get("action")) {
      setDisplaySuccessMessage({
        type: params.get("action"),
        show: true,
      });
      router.replace("/");
    }
  }, [params, router]);

  // Automatically hide the success message after 3 seconds
  useEffect(() => {
    const timer = setTimeout(() => {
      if (displaySuccessMessage.show) {
        setDisplaySuccessMessage({ show: false, type: "" });
      }
    }, 3000);
    return () => clearTimeout(timer);
  }, [displaySuccessMessage.show]);

  // Handle deletion of a menu item
  const handleDelete = (id) => {
    setMenuItems((items) => items.filter((item) => item.id !== id));
  };

  return (
    <div>
      <button className="add-button" onClick={() => router.push("/add")}>
        Add
      </button>
      {displaySuccessMessage.show && (
        <p className="success-message">
          {displaySuccessMessage.type === "add" ? "Added a" : "Modified a"} menu
          item.
        </p>
      )}
      {menuItems ? (
        menuItems.map((item) => (
          <MenuItem
            key={item.id}
            id={item.id}
            name={item.name}
            price={item.price}
            onEdit={() => router.push(`/update/${item.id}`)}
            onDelete={handleDelete}
          />
        ))
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

The block code above is quite long, but let's quickly describe what is going on there.

  • Page is the name of the component. Next.js will display on the browser the code coming from this component.

  • Next, we are defining important states such as menuItems to stock the response we will get from getData and the displaySuccessMessage that will be used to show feedback when a creation/deletion is successful. We are also retrieving objects such as the router and the params. Those will be useful in implementing the deletion logic and the routing for the creation or edition of a menu.

        const [menuItems, setMenuItems] = useState(null);
        const router = useRouter();
        const params = useSearchParams();
    
        // State for displaying a success message
        const [displaySuccessMessage, setDisplaySuccessMessage] = useState({
          show: false,
          type: "", // either 'add' or 'update'
        });
    
  • In the next lines of code, we are defining three useEffect hooks, the first one to help us retrieve data from the API by calling the getData method, the second one used to handle success message displays when a creation or an update is successful ( we use a param in the URL to check if we need to display the message ) and the last useEffect is used to handle the time of display of the message. We use the setTimeout method to display the message for 3 seconds only

  • Next, we are writing the JSX code with the add button. We are also using the MenuItem component declared above in the page.js file to display menu information.

You can find the code for the whole file on the Github repository here.

We have a working listing page now. We can also delete articles. Let's now write the creation page.

Building the Creation Page for a menu

In the last section, we have built the page for listing all menus. In this section, we will build the page for the creation of a Menu.

This will just be a form with a name and price fields. Let's get to it.

In the src/app directory, create a new directory called add. Inside this newly created directory, create a file called page.js. With this file, it means that when we navigate to the /add route, we will have the code on the src/app/add/page.js displayed.

Let's start writing the code.

// src/app/add/page.js

"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

/**
 * Sends a POST request to create a new menu item.
 * @param {Object} data The menu item data to be sent.
 */
async function createMenu(data) {
  const res = await fetch("http://127.0.0.1:8000/api/menu/", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });

  if (!res.ok) {
    throw new Error("Failed to create data");
  }

  return res.json();
}

In the code above, we are importing the required hooks for managing side effects, states, and routing. The next code block is much more interesting, as we are writing the method createMenu in charge of sending the POST request for the creation of a menu. This method takes as a parameter data which is a JSON object containing the required data to create a menu object.

We can move now to writing the Page component logic for this page.

// src/app/add/page.js
...
const Page = () => {
  const router = useRouter();
  const [formData, setFormData] = useState({ name: "", price: "" });
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  /**
   * Handles the form submission.
   * @param {Event} event The form submission event.
   */
  const onFinish = (event) => {
    event.preventDefault();
    setIsLoading(true);
    createMenu(formData)
      .then(() => {
        // Navigate to the main page with a query parameter indicating success
        router.replace("/?action=add");
      })
      .catch(() => {
        setError("An error occurred");
        setIsLoading(false);
      });
  };

  // Cleanup effect for resetting loading state
  useEffect(() => {
    return () => setIsLoading(false);
  }, []);

  return (
    <form onSubmit={onFinish}>
      <div className="form-item">
        <label htmlFor="name">Name</label>
        <input
          required
          name="name"
          value={formData.name}
          onChange={(event) =>
            setFormData({ ...formData, name: event.target.value })
          }
        />
      </div>
      <div className="form-item">
        <label htmlFor="price">Price</label>
        <input
          required
          type="number"
          name="price"
          value={formData.price}
          onChange={(event) =>
            setFormData({ ...formData, price: event.target.value })
          }
        />
      </div>
      {error && <p className="error-message">{error}</p>}
      <div>
        <button disabled={isLoading} className="add-button" type="submit">
          Submit
        </button>
      </div>
    </form>
  );
};

export default Page;

The code above is also quite long, but let's explore what is done there.

  • We are defining three states for managing the form data formData, loading when a request is pending loading , and also one state to store errors received from the backend and display them on the frontend error.

  • Next, we have the onFinish method which is the method executed when the user submits the form. This method will first call the event.preventDefault(); to prevent the default behavior of the browser when the user clicks on the submit button of a form, which reloads the page.

    After that, we set the loading state to true, as we are starting a creation request. Because this is an asynchronous request, we handle cases where the request is successful, by redirecting the user to the listing page with the URL param action=add which will trigger the display of a success message. In the case of an error, we set the error message that will be displayed on the frontend.

  • Next, we have a useEffect used to clean up effect if the user leaves the page for example, or when the component is unmounted.

  • Finally, the JSX code where we use conventional HTML tags to build a form, pass required props values to the inputs, and display the error message.

With the creation page written, we can finally move to crafting the edition page. Nothing will change as much from the creation page apart from the fact that we must:

  • Retrieve the menu with the id on the editing page to fill the form with the existing values so that the user can modify them.

  • And that's it mostly.

Let's create the edition page.

Creating the Edition page

In the precedent section, we have learned how to create a simple form with Next.js and how to send a request to an API using the fetch API. We have now a working creation page for adding a new menu.

In this section, we will build the edition page for modifying the name, and the price of a menu. Nothing different from the creation page apart from the routing and the fact that we need information about the menu we want to modify.

In the src/app/ directory, create a directory called update . In this directory, create a new directory called [menuId]. This tells Next.js that this is a dynamic route because the menuId can be changed depending on the item selected for editing in the listing of menus.

So for example, the user can be redirected to /update/1 or /update/2 with menuId being either 1 or 2. This will also help us retrieve the menuId from the URL to request the API to retrieve data about the menu from the backend, and we can fill the form with the values that we have for price and name in the form.

In the src/app/update/[menuId]/ directory, create the page.js file.

// src/app/update/[menuId]/page.js

"use client" 

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

/**
 * Fetches a menu item by ID.
 * @param {number} id The ID of the menu item to retrieve.
 */
async function getMenu(id) {
  const res = await fetch(`http://127.0.0.1:8000/api/menu/${id}/`);
  if (!res.ok) {
    throw new Error("Failed to retrieve menu");
  }
  return res.json();
}

/**
 * Updates a menu item by ID.
 * @param {number} id The ID of the menu item to update.
 * @param {Object} data The updated data for the menu item.
 */
async function updateMenu(id, data) {
  const res = await fetch(`http://127.0.0.1:8000/api/menu/${id}/`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });

  if (!res.ok) {
    throw new Error("Failed to update menu");
  }
  return res.json();
}

In the code above, we are importing the required hooks for managing side effects, states, and routing. In the next code block, we are defining two methods :

  • getMenu to retrieve a specific menu from the API using the detail API endpoint.

  • updateMenu to send a PUT request to update information about a specific menu.

Let's move to the code of the Page component.

// src/app/update/[menuId]/page.js

... 

const Page = ({ params }) => {
  const router = useRouter();
  const [formData, setFormData] = useState({ name: "", price: "" });
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  /**
   * Handles form submission.
   * @param {Event} event The form submission event.
   */
  const onFinish = (event) => {
    event.preventDefault();
    setIsLoading(true);
    updateMenu(params.menuId, formData)
      .then(() => {
        router.replace("/?action=update");
      })
      .catch(() => {
        setError("An error occurred");
        setIsLoading(false);
      });
  };

  // Cleanup effect for resetting loading state
  useEffect(() => {
    return () => setIsLoading(false);
  }, []);

  // Fetch menu item data on component mount
  useEffect(() => {
    const fetchData = async () => {
      try {
        const data = await getMenu(params.menuId);
        setFormData({ name: data.name, price: data.price });
      } catch (error) {
        setError(error.message);
      }
    };
    fetchData();
  }, [params.menuId]);

  return (
    <form onSubmit={onFinish}>
      <div className="form-item">
        <label htmlFor="name">Name</label>
        <input
          required
          name="name"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        />
      </div>
      <div className="form-item">
        <label htmlFor="price">Price</label>
        <input
          required
          type="number"
          name="price"
          value={formData.price}
          onChange={(e) => setFormData({ ...formData, price: e.target.value })}
        />
      </div>
      {error && <p className="error-message">{error}</p>}
      <div>
        <button disabled={isLoading} className="add-button" type="submit">
          Submit
        </button>
      </div>
    </form>
  );
};

export default Page;

The code above is nearly identical to the form for the creation of the menu, the main difference being the prop passed to the Page component.

In Next.js 13, the params object is the prop containing the dynamic segment values (in our case menuId). With the value of menuId, we can easily trigger the getMenu the method by passing the params.menuId value but also ensure the update request with the updateMenu method by passing the params.menuId value and the data retrieved from the form.

This is great! We have a fully working Next.js 13 application integrated with Django that can list menus, and display pages for the creation and editing of menu information, but can also handle the deletion. Here's below, a demo of how the application features should look like.

Conclusion

And there you have it โ€“ a step-by-step guide to building a full-stack application using Django and Next.js. This combination offers a robust and scalable solution for web development, blending Django's secure and efficient backend capabilities with the reactive and modern frontend prowess of Next.js.

By following this guide, you've learned how to set up a Django REST API and create a frontend application in Next.js. This CRUD application for managing a restaurant's menu serves as a practical example of how these two technologies can be seamlessly integrated.

Remember, the journey doesn't end here. Both Django and Next.js are rich with features and possibilities. I encourage you to dive deeper, experiment with more advanced features, and continue honing your skills as a full-stack developer.

Here is the link to the codebase of the application built in this article.