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 ProductList from "./components/ProductList"; import { getProductList } from "./api/product"; import LoadingSpinner from "./components/LoadingSpinner"; import Paginationnumber from "./components/Paginationnumber"; import { useSearchParams } from 'next/navigation' export default function Home() { const searchParams = useSearchParams() const page = searchParams.get('page') const currentPage = Number(page) || 1; //const currentPage = 1; const { isLoading, data, isError, isFetching, error } = useQuery({ queryKey: ["products", currentPage], queryFn: () => getProductList(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(data) 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-5"> <h1 className="text-4xl font-bold">Nextjs 15 Laravel 11 Rest API Pagination View Product | Tanstack Query Tailwind CSS</h1> </div> <div className="overflow-x-auto py-10"> <ProductList productlist={data.postlist} /> <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> ); }app\components\ProductList.tsx
//app\components\ProductList.tsx import Image from 'next/image' const ProductList = ({ productlist }) => { return ( <> <div className="grid md:grid-cols-3 gap-5 mt-10"> {productlist.map((item) => ( <div key={item.id} className="bg-white rounded-lg shadow-lg p-8"> <div className="relative overflow-hidden"> <Image src={`http://127.0.0.1:8000/storage/${item.image}`} width={400} height={400} alt="Photo" /> <div className="absolute inset-0 bg-black opacity-40" /> <div className="absolute inset-0 flex items-center justify-center"> <a className="bg-white text-gray-900 py-2 px-6 rounded-full font-bold hover:bg-gray-300" href={`/product/${item.id}`}> View Product </a> </div> </div> <h3 className="text-xl font-bold text-gray-900 mt-4">{item.name}</h3> <p className="text-gray-500 text-sm mt-2">Description: {item.name}</p> <div className="flex items-center justify-between mt-4"> <span className="text-gray-900 font-bold text-lg">${item.price}.99</span> <button className="bg-gray-900 text-white py-2 px-4 rounded-full font-bold hover:bg-gray-800">Add to Cart</button> </div> </div> ))} </div> </> ) } export default ProductListapp\components\LoadingSpinner.tsx
//app\components\LoadingSpinner.tsx export default function LoadingSpinner() { return ( <h1>Loading...</h1> ) }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-100 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;app\product\[id]\page.tsx
//app\product\[id]\page.tsx "use client"; import { useQuery } from "@tanstack/react-query"; //npm i @tanstack/react-query import { useParams } from 'next/navigation' import { fetchProduct } from "../../api/product"; import Image from 'next/image' const Viewproduct = () => { const {id}=useParams(); const { isLoading, isError, data: product, error, } = useQuery({ queryKey: ["product", id], queryFn: () => fetchProduct(id), }); if (isLoading) return "loading..."; if (isError) return `Error: ${error.message}`; console.log(product) 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 Product</h1> <table className="table table-zebra"> <thead className="text-sm text-gray-700 uppercase bg-gray-50"> <tr> <th>S No.</th> <th>Product Name</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td>{product.product.id}</td> <td>{product.product.name}</td> <td>{product.product.price}</td> </tr> </tbody> </table> <p className="text-center mt-6"> <Image src={`http://127.0.0.1:8000/storage/${product.product.image}`} width={200} height={200} alt="Photo" style={{width:'400px', height: "auto" }} /> </p> </div> </div> </div> ) } export default Viewproductapp\api\product.tsx
//app\api\product.tsx export async function getProductList(page) { const response = await fetch(`http://127.0.0.1:8000/api/products?page=${page}`); const data = await response.json(); //console.log(data.results.total); return { postlist: data.results.data, totalpage: data.results.total, perpage: data.results.per_page } } export async function fetchProduct(id) { const response = await fetch("http://127.0.0.1:8000/api/products/"+id); return response.json() }next.config
//next.config import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, images : { domains : ['localhost', 'cairocdoers-ednalan.com', '127.0.0.1'] // == Domain name } }; export default nextConfig;run C:\nextjs>npm run dev
Download Laravel App
https://laravel.com/docs/11.x/installation
Connecting our Database
open .env file root directory.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=laravel11dev
DB_USERNAME=root
DB_PASSWORD=root
Create Model and Migration
C:\xampp\htdocs\laravel\laravelproject>php artisan make:model Product -m
A new file named Product.php will be created in the app directory and database/migrations directory to generate the table in our database
app/Models/Product.php
//app/Models/Product.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Product extends Model { use HasFactory; protected $fillable = [ 'name', 'image', 'price' ]; }database\migrations\create_products_table.php
//database\migrations\create_products_table.ph <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('image'); $table->integer('price'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('products'); } };Database Migration
php artisan migrate
C:\xampp\htdocs\laravel\laravel10project>php artisan migrate
Migration table created successfully.
check database table
Create Controller and Request
C:\xampp\htdocs\laravel\laravel10project>php artisan make:controller ProductController
app\Http\Controllers\ProductController.php
//app\Http\Controllers\ProductController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Product; use Illuminate\Support\Facades\Storage; //php artisan storage:link = php artisan storage:link = http://127.0.0.1:8000/storage/1.jpg class ProductController extends Controller { public function index() { //$products = Product::all(); // All Product $products = Product::paginate(3); // Return Json Response return response()->json([ 'results' => $products ], 200); } public function show($id) { // Product Detail $product = Product::find($id); if (!$product) { return response()->json([ 'message' => 'Product Not Found.' ], 404); } // Return Json Response return response()->json([ 'product' => $product ], 200); } }Routes
install
php artisan install:api
All API requests will need the header Accept: application/json.
open routes/api.php and update the following code
routes\api.php
//routes\api.php <?php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\ProductController; Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); Route::get('products', [ProductController::class, 'index']); Route::get('products/{id}', [ProductController::class, 'show']);Run C:\xampp\htdocs\laravel\myapp>php artisan serve
Starting Laravel development server: http://127.0.0.1:8000