Django and Nextjs are the one 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 plain 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 menuprice
for the pricecreated
for the date of creation of the object. Withauto_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. Withauto_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 DRFViewSet
.MenuViewSet
frommenu.viewsets
is registered with the router.The
basename
parameter inrouter.register
is set to"menu"
. This basename is used to construct the URL names for theMenuViewSet
.Finally,
urlpatterns = router.urls
sets the urlpatterns for this part of the application to those generated by the router for theMenuViewSet
.
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.
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
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 http://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 apages
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 aserror.js
for error handling andloading.js
for default loading behaviors.In this framework, creating a route like
menu/supplements
on the client side in a Next.js application involves placing apage.js
file in the correspondingmenu/supplements
directory. This focus onpage.js
files simplifies 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, thelayout.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 ownlayout.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 thelayout.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, theid
of the article, and then sends a deletion request. We are using thefetch
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 fromgetData
and thedisplaySuccessMessage
that will be used to show feedback when a creation/deletion is successful. We are also retrieving objects such as therouter
and theparams
. 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 thegetData
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 lastuseEffect
is used to handle the time of display of the message. We use thesetTimeout
method to display the message for 3 seconds onlyNext, we are writing the JSX code with the add button. We are also using the
MenuItem
component declared above in thepage.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 pendingloading
, and also one state to store errors received from the backend and display them on the frontenderror
.-
Next, we have the
onFinish
method which is the method executed when the user submits the form. This method will first call theevent.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 paramaction=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 effects 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.