Install nextjs npx create-next-app@latest nextjs_org/docs/getting-started/installation
Install react-query
npm i @tanstack/react-query
tanstack com/query/latest/docs/framework/react/installation
app\page.tsx
//app\page.tsx
"use client";
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query tanstack com/query/latest/docs/framework/react/installation
import CustomerList from "./components/CustomerList";
import { getCustomerList } from "./api/customer";
import Paginationnumber from "./components/Paginationnumber";
import LoadingSpinner from "./components/LoadingSpinner";
import { useSearchParams } from 'next/navigation'
import Link from "next/link";
export default function Home() {
const searchParams = useSearchParams()
const page = searchParams.get('page')
//console.log(page)
const currentPage = Number(page) || 1;
const { isLoading, data, isError, isFetching, error } = useQuery({
queryKey: ["customers", currentPage],
queryFn: () => getCustomerList(currentPage)
});
console.log(data);
if (isLoading) return "Loading...";
if (isError) return `Error: ${error.message}`;
const totalPages = Math.ceil(Number(data.totalpage) / Number(data.perpage));
//console.log(totalPages);
return (
<div className="w-screen py-20 flex justify-center flex-col items-center">
<div className="flex items-center justify-between gap-1 mb-5 pl-10 pr-10">
<h1 className="text-4xl font-bold">Nextjs 15 Python Django CRUD (Create, Read, Update and Delete) with Pagination | Tanstack Query Tailwind CSS</h1>
</div>
<div className="overflow-x-auto pt-10">
<div className="mb-2 w-full text-right">
<Link
href="/customer/create"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
Add New
</Link>
</div>
<CustomerList customerlist={data.customerlist} />
<div className="flex items-center justify-between my-5">
<Paginationnumber totalPages={totalPages} />
{isFetching ? <LoadingSpinner /> : null}
</div>
</div>
</div>
);
}
app\layout.tsx
//app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import QueryProvider from "./components/QueryProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}
app\components\QueryProvider.tsx
//app\components\QueryProvider.tsx
"use client";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
children: React.ReactNode;
}
export default function QueryProvider({ children }: Props) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
);
}
Table List : app\components\CustomerList.tsx
//app\components\CustomerList.tsx
import Link from "next/link";
import { useMutation, useQueryClient } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { deleteCustomer } from "../api/customer";
const CustomerList = ({ customerlist }) => {
const queryClient = useQueryClient();
const deleteCustomerMutation = useMutation({
mutationFn: deleteCustomer,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['customers']});
}
});
const handleDelete = (id) => {
deleteCustomerMutation.mutate(id)
}
return (
<>
<table className="table table-zebra">
<thead className="text-sm text-gray-700 uppercase bg-gray-50">
<tr>
<th className="py-3 px-6">ID</th>
<th className="py-3 px-6">Name</th>
<th className="py-3 px-6">Email</th>
<th className="py-3 px-6 text-center">Actions</th>
</tr>
</thead>
<tbody>
{customerlist.map((item) => (
<tr key={item.id} className="bg-white border-b text-black">
<td className="py-3 px-6">
{item.id}
</td>
<td className="py-3 px-6">{item.name}</td>
<td className="py-3 px-6">{item.email}</td>
<td className="flex justify-center gap-1 py-3">
<Link className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
href={`/customer/${item.id}`}>Read</Link>
<Link className="focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:focus:ring-yellow-900"
href={`/customer/edit/${item.id}/`}>
Edit
</Link>
<button onClick={() => handleDelete(item.id)} className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</>
)
}
export default CustomerList
Loader : app\components\LoadingSpinner.tsx
//app\components\LoadingSpinner.tsx
export default function LoadingSpinner() {
return (
<h1>Loading...</h1>
)
}
Create page : app\customer\create\page.tsx
//app\customer\create\page.tsx
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query"
import CustomertForm from "../../components/CustomertForm"
import { createCustomer } from "../../api/customer"
import { useRouter } from 'next/navigation'
const AddCustomer= () => {
const queryClient = useQueryClient();
const router = useRouter();
const createUserMutation = useMutation({
mutationFn: createCustomer,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['customers']});
//console.log("success!")
router.push('/')
}
});
const handleAddPost = (customer) => {
//console.log(customer)
createUserMutation.mutate({
...customer
})
}
return (
<div className="max-w-md mx-auto mt-5 mb-5">
<h1 className="text-2xl text-center mb-2">Add New Customer</h1>
<CustomertForm onSubmit={handleAddPost} initialValue={{}} />
</div>
)
}
export default AddCustomer
Form : app\components\CustomertForm.tsx
//app\components\CustomertForm.tsx
"use client";
import { useState } from "react"
const CustomertForm = ({ onSubmit, initialValue }) => {
const [customer, setCustomer] = useState({
name: initialValue.name || "",
email: initialValue.email || "",
contactnumber: initialValue.contactnumber || ""
});
//console.log(customer);
const handleChangeInput = (e) => {
setCustomer({
...customer,
[e.target.name]: e.target.value
})
}
const renderField = (label) => (
<div>
<label className="block text-sm font-medium text-gray-900">{label}</label>
<input onChange={handleChangeInput} type="text" name={label.toLowerCase()} value={customer[label.toLowerCase()]}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500blue-500"
/>
</div>
);
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(customer);
setCustomer({
name: "",
email: "",
contactnumber: "",
})
}
return (
<form onSubmit={handleSubmit}>
<div className="mb-5">{renderField('Name')}</div>
<div className="mb-5">{renderField('Email')}</div>
<button type="submit"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Submit</button>
</form>
)
}
export default CustomertForm
Read : app\customer\[id]\page.tsx
//app\customer\[id]\page.tsx
"use client";
import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query
import { useParams } from 'next/navigation'
import { fetchCustomer } from "../../api/customer";
const ViewCustomer = () => {
const {id}=useParams();
//console.log(id);
const {
isLoading,
isError,
data: customer,
error,
} = useQuery({
queryKey: ["customer", id],
queryFn: () => fetchCustomer(id),
});
if (isLoading) return "loading...";
if (isError) return `Error: ${error.message}`;
console.log(customer);
return (
<div className="min-h-screen flex items-center justify-center bg-slate-100">
<div className="bg-white rounded-sm shadow p-8 text-black">
<div className="max-w-2xl mx-auto mt-5">
<h1 className="text-2xl text-center mb-2">View Customer</h1>
<h2>ID : {customer.getcustomer.id}</h2>
<h2>Name : {customer.getcustomer.name}</h2>
<h2>Email : {customer.getcustomer.email}</h2>
</div>
</div>
</div>
)
}
export default ViewCustomer
Edit : app\customer\edit\[id]\page.tsx
//app\customer\edit\[id]\page.tsx
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useParams, useRouter } from 'next/navigation'
import { fetchCustomer, updateCustomer } from "../../../api/customer";
import CustomertForm from "../../../components/CustomertForm"
const EditCustomer = () => {
const {id}=useParams();
//console.log(id);
const queryClient = useQueryClient();
const router = useRouter();
const {
isLoading,
isError,
data: customer,
error,
} = useQuery({
queryKey: ["customers", id],
queryFn: () => fetchCustomer(id),
});
console.log(customer);
const updateCustomerMutation = useMutation({
mutationFn: updateCustomer,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['customers']});
console.log("success");
router.push('/')
}
})
if (isLoading) return "loading...";
if (isError) return `Error: ${error.message}`;
const handleSubmit = (updatedCustomer) => {
updateCustomerMutation.mutate({id, ...updatedCustomer})
}
return (
<div className="max-w-md mx-auto mt-5">
<div className="flex items-center justify-between gap-1 mb-5">
<h1 className="text-4xl font-bold">{customer.getcustomer.name}</h1>
</div>
<CustomertForm onSubmit={handleSubmit} initialValue={customer.getcustomer} />
</div>
)
}
export default EditCustomer
Pagination : app\components\Paginationnumber.tsx
//app\components\Paginationnumber.tsx
"use client";
import Link from "next/link";
import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; //npm install react-icons --save npmjs com/package/react-icons
import { usePathname, useSearchParams } from "next/navigation";
import clsx from "clsx"; //npm i clsx npmjs com/package/clsx
export const generatePagination = (currentPage: number, totalPages: number) => {
if (totalPages <= 7) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
if (currentPage <= 3) {
return [1, 2, 3, "...", totalPages - 1, totalPages];
}
if (currentPage >= totalPages - 2) {
return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
}
return [
1,
"...",
currentPage - 1,
currentPage,
currentPage + 1,
"...",
totalPages,
];
};
const Paginationnumber = ({ totalPages }: { totalPages: number }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get("page")) || 1;
const createPageURL = (pageNumber: string | number) => {
const params = new URLSearchParams(searchParams);
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
const allPages = generatePagination(currentPage, totalPages);
const PaginationNumber = ({
page,
href,
position,
isActive,
}: {
page: number | string;
href: string;
position?: "first" | "last" | "middle" | "single";
isActive: boolean;
}) => {
const className = clsx(
"flex h-10 w-10 items-center justify-center text-sm border",
{
"rounded-l-sm": position === "first" || position === "single",
"rounded-r-sm": position === "last" || position === "single",
"z-10 bg-blue-700 border-blue-500 text-white bg-blue-700": isActive,
"hover:bg-blue-700": !isActive && position !== "middle",
"text-gray-300 pointer-events-none": position === "middle",
}
);
return isActive && position === "middle" ? (
<div className={className}>{page}</div>
) : (
<Link href={href} className={className}>
{page}
</Link>
);
};
const PaginationArrow = ({
href,
direction,
isDisabled,
}: {
href: string;
direction: "left" | "right";
isDisabled?: boolean;
}) => {
const className = clsx(
"flex h-10 w-10 items-center justify-center text-sm border",
{
"pointer-events-none text-blue-300": isDisabled,
"hover:bg-blue-700": !isDisabled,
"mr-2": direction === "left",
"ml-2": direction === "right",
}
);
const icon =
direction === "left" ? (
<HiChevronLeft size={20} />
) : (
<HiChevronRight size={20} />
);
return isDisabled ? (
<div className={className}>{icon}</div>
) : (
<Link href={href} className={className}>
{icon}
</Link>
);
};
return (
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>
<div className="flex -space-x-px">
{allPages.map((page, index) => {
let position: "first" | "last" | "single" | "middle" | undefined;
if (index === 0) position = "first";
if (index === allPages.length - 1) position = "last";
if (allPages.length === 1) position = "single";
if (page === "...") position = "middle";
return (
<PaginationNumber
key={index}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>
<PaginationArrow
direction="right"
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
</div>
);
};
export default Paginationnumber;
Route API : app\api\customer.tsx
//app\api\customer.tsx
export async function getCustomerList(page) {
const response = await fetch(`http://localhost:8000/api/customers/?page=${page}`);
//const response = await fetch("http://localhost:8000/api/customers/");
const data = await response.json();
//console.log(data);
return {
customerlist: data.results, //customerlist: data,
totalpage: data.count,
perpage: 3
}
}
export async function fetchCustomer(id) {
const response = await fetch(`http://localhost:8000/api/customers/${id}/`);
const data = await response.json();
//console.log(data);
return {
getcustomer: data
}
}
export async function createCustomer(newCustomer) {
const response = await fetch("http://localhost:8000/api/customers/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newCustomer)
});
console.log(response);
return response.json()
}
export async function updateCustomer(updatedCustomer) {
const response = await fetch(`http://localhost:8000/api/customers/${updatedCustomer.id}/`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(updatedCustomer)
});
console.log(response)
return response.json()
}
export async function deleteCustomer(id) {
const response = await fetch(`http://localhost:8000/api/customers/${id}/`, {
method: "DELETE",
});
return response.json()
}
run C:\nextjs>npm run dev Step 1 — Setting Up the Backend
Now install Pipenv using pip:
ednalan@Cairocoders django-nextjs % pip install pipenv
And activate a new virtual environment:
pipenv shell
ednalan@Cairocoders django-nextjs % pipenv shell
Install Django using Pipenv:
pipenv install django
ednalan@Cairocoders django-nextjs % pipenv install django
Then create a new project called backend:
django-admin startproject backend
ednalan@Cairocoders django-nextjs % django-admin startproject backend
navigate into the newly created backend directory:
cd backend
ednalan@Cairocoders backend %
Start a new application called myapp:
ednalan@Cairocoders backend % python manage.py startapp myapp
Run migrations:
ednalan@Cairocoders backend % python manage.py migrate
Navigate to http://localhost:8000 in your web browser:
Registering the myapp Application
Open the backend/settings.py file in your code editor and add myapp to the INSTALLED_APPS:
//backend/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'myapp',
]
save your changes.Defining the Customer Model
Open the myapp/models.py file in your code editor and add the following lines of code:
//myapp/models.py
from django.db import models
# Create your models here.
class Customer(models.Model):
name = models.CharField(max_length=124)
email = models.CharField(max_length=125)
def _str_(self):
return self.name
Create a migration file python manage.py makemigrations myapp
And apply the changes to the database: python manage.py migrate myapp Open the myapp/admin.py file with your code editor and add the following lines of code:
//myapp/admin.py
from django.contrib import admin
from .models import Customer
class CustomerAdmin(admin.ModelAdmin):
list_display = ('name', 'email')
# Register your models here.
admin.site.register(Customer, CustomerAdmin)
create a “superuser” account to access the admin interface. Run the following command in your terminal: python manage.py createsuperuser
Start the server once again:
python manage.py runserver
Navigate to http://localhost:8000/admin in your web browser. And log in with the username and password that was created You can create, edit, and, delete items using this interface:
Step 2 — Setting Up the APIs create an API using the Django REST framework.
Install the djangorestframework and django-cors-headers using Pipenv:
pipenv install djangorestframework django-cors-headers
need to add rest_framework and corsheaders to the list of installed applications. Open the backend/settings.py file in your code editor and update the INSTALLED_APPS and MIDDLEWARE sections:
INSTALLED_APPS = [
'corsheaders',
'rest_framework',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
add these lines of code to the bottom of the backend/settings.py file:
CORS_ORIGIN_WHITELIST = [
'http://localhost:3000'
]
Creating serializers
serializers to convert model instances to JSON so that the frontend can work with the received data.
myapp/serializers.py file with your code editor. Open the serializers.py file and update it with the following lines of code:
//myapp/serializers.py
from rest_framework import serializers
from .models import Customer
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ('id', 'name', 'email')
This code specifies the model to work with and the fields to be converted to JSON.Creating View
myapp/views.py
//myapp/views.py
from django.shortcuts import render
from rest_framework import viewsets
from .serializers import CustomerSerializer
from .models import Customer
# Create your views here.
class CustomerView(viewsets.ModelViewSet):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
Open the backend/urls.py file with your code editor and replace the contents with the following lines of code:
//backend/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from myapp import views
router = routers.DefaultRouter()
router.register(r'customers', views.CustomerView, 'customer')
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
]
restart the server:python manage.py runserver
Navigate to http://localhost:8000/api/customers http://localhost:8000/api/customers/1 in your web browser:
Pagination
django-rest-framework
PageNumberPagination
Request:
GET https://api.example.org/accounts/?page=4
Setup
To enable the PageNumberPagination style globally, use the following configuration, and set the PAGE_SIZE as desired:
backend/settings.py
REST_FRAMEWORK = { #https://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 3
}
http://localhost:8000/api/customers/?page=1
