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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //app\page.tsx "use client" ; import { useQuery } from "@tanstack/react-query" ; //npm i @tanstack/react-query https://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 Flask Rest API 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> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | //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> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //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> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //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" >Contact No.</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= "py-3 px-6" >{item.contactnumber}</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 |
1 2 3 4 5 6 | //app\components\LoadingSpinner.tsx export default function LoadingSpinner() { return ( <h1>Loading...</h1> ) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //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> <div className= "mb-5" >{renderField( 'Contactnumber' )}</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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //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(); const { isLoading, isError, data: customer, error, } = useQuery({ queryKey: [ "customer" , id], queryFn: () => fetchCustomer(id), }); //console.log(customer); 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.customer.id}</h2> <h2>Name : {customer.customer.name}</h2> <h2>Email : {customer.customer.email}</h2> <h2>Contact Number : {customer.customer.contactnumber}</h2> </div> </div> </div> ) } export default ViewCustomer |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //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.customer.name}</h1> </div> <CustomertForm onSubmit={handleSubmit} initialValue={customer.customer} /> </div> ) } export default EditCustomer |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | //app\api\customer.tsx export async function getCustomerList(page) { const response = await fetch(`http: //127.0.0.1:5000/api/customerpagination/${page}/3`); const data = await response.json(); //console.log(data); return { customerlist: data.customers, totalpage: data.total, perpage: data.per_page } } export async function fetchCustomer(id) { return response.json() } export async function createCustomer(newCustomer) { 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: //127.0.0.1:5000/api/customer/${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: //127.0.0.1:5000/api/customer/${id}`, { method: "DELETE" , }); return response.json() } |
python-guide-es.readthedocs.io/es/guide-es/dev/virtualenvs.html
Create an environment
ednalan@Cairocoders myapp % pip install virtualenv
ednalan@Cairocoders myapp % pip install virtualenv
Activate the environment
ednalan@Cairocoders myapp % source venv/bin/activate
(venv) ednalan@Cairocoders myapp %
Install Flask
pypi.org/project/Flask/
(venv) ednalan@Cairocoders myapp % pip install -U Flask
(venv) ednalan@Cairocoders myapp % flask run
Install requirements
pip install -U flask-cors
pypi.org/project/Flask-Cors/
Flask-SQLAlchemy
Flask-SQLAlchemy is an extension for Flask that adds support for SQLAlchemy to your application.
flask-sqlalchemy.palletsprojects.com/en/3.0.x/
(venv) PS C:\flask_dev\myapp>pip install -U Flask-SQLAlchemy
Flask + marshmallow for beautiful APIs
pypi.org/project/flask-marshmallow/
(venv) PS C:\flask_dev\myapp>pip install flask-marshmallow
python3 -m pip install
https://pypi.org/project/pymysql/
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | / / app.py from flask import Flask, request, jsonify, make_response from flask_cors import CORS, cross_origin #ModuleNotFoundError: No module named 'flask_cors' = pip install Flask-Cors from models import db, Customer app = Flask(__name__) app.config[ 'SECRET_KEY' ] = 'cairocoders-ednalan' #app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///flaskdb.db' # Databse configuration mysql Username:password@hostname/databasename SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = True CORS(app, supports_credentials = True ) db.init_app(app) with app.app_context(): db.create_all() # create a test route @app .route( '/test' , methods = [ 'GET' ]) def test(): return jsonify({ 'message' : 'The server is running' }) # create a customer @app .route( '/api/customer' , methods = [ 'POST' ]) def create_customer(): try : data = request.get_json() new_customer = Customer(name = data[ 'name' ], email = data[ 'email' ], contactnumber = data[ 'contactnumber' ]) db.session.add(new_customer) db.session.commit() return jsonify({ 'id' : new_customer. id , 'name' : new_customer.name, 'email' : new_customer.email, 'contactnumber' : new_customer.contactnumber }), 201 except Exception as e: return make_response(jsonify({ 'message' : 'error creating customer' , 'error' : str (e)}), 500 ) # get all customer @app .route( '/api/customer' , methods = [ 'GET' ]) def get_customer(): try : customer = Customer.query. all () customer_data = [{ 'id' : customer. id , 'name' : customer.name, 'email' : customer.email} for customer in users] return jsonify(customer_data), 200 except Exception as e: return make_response(jsonify({ 'message' : 'error getting customer' , 'error' : str (e)}), 500 ) @app .route( '/api/customerpagination/<int:page>/<int:per_page>' , methods = [ 'GET' ]) def customerpagination(page = 1 , per_page = 3 ): total = Customer.query.count() print (total) customers = Customer.query.order_by(Customer. id .asc()) customers = customers.paginate(page = page, per_page = per_page) return jsonify({ 'total' : total, 'page' : page, 'per_page' : per_page, 'has_next' : customers.has_next, 'has_prev' : customers.has_prev, 'page_list' : [iter_page if iter_page else '...' for iter_page in customers.iter_pages()], 'customers' : [{ 'id' : p. id , 'name' : p.name, 'email' : p.email, 'contactnumber' : p.contactnumber, } for p in customers.items] }) # get a customers by id @app .route( '/api/customer/<id>' , methods = [ 'GET' ]) def get_customerid( id ): try : customer = Customer.query.filter_by( id = id ).first() # get the first customer with the id if customer: return make_response(jsonify({ 'customer' : customer.json()}), 200 ) return make_response(jsonify({ 'message' : 'customer not found' }), 404 ) except Exception as e: return make_response(jsonify({ 'message' : 'error getting customer' , 'error' : str (e)}), 500 ) # update a customer by id @app .route( '/api/customer/<id>' , methods = [ 'PUT' ]) def update_customer( id ): try : customer = Customer.query.filter_by( id = id ).first() if customer: data = request.get_json() customer.name = data[ 'name' ] customer.email = data[ 'email' ] customer.contactnumber = data[ 'contactnumber' ] db.session.commit() return make_response(jsonify({ 'message' : 'customer updated' }), 200 ) return make_response(jsonify({ 'message' : 'customer not found' }), 404 ) except Exception as e: return make_response(jsonify({ 'message' : 'error updating customer' , 'error' : str (e)}), 500 ) # delete a customer by id @app .route( '/api/customer/<id>' , methods = [ 'DELETE' ]) def delete_customer( id ): try : customer = Customer.query.filter_by( id = id ).first() if customer: db.session.delete(customer) db.session.commit() return make_response(jsonify({ 'message' : 'customer deleted' }), 200 ) return make_response(jsonify({ 'message' : 'customer not found' }), 404 ) except Exception as e: return make_response(jsonify({ 'message' : 'error deleting customer' , 'error' : str (e)}), 500 ) if __name__ = = "__main__" : app.run(debug = True ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | / / models.py from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Customer(db.Model): __tablename__ = 'customer' id = db.Column(db.Integer, primary_key = True ) name = db.Column(db.String( 80 ), unique = True , nullable = False ) email = db.Column(db.String( 120 ), unique = True , nullable = False ) contactnumber = db.Column(db.String( 80 ), unique = True , nullable = False ) def json( self ): return { 'id' : self . id , 'name' : self .name, 'email' : self .email, 'contactnumber' : self .contactnumber} |