Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124

How to Build a Server-Side Pagination System with Next.js 14+ App Router

Server-side pagination is the heavy lifter’s choice for managing large datasets efficiently. It reduces the load on the client, improves initial page load times, and plays nicely with SEO. Let’s dive into building a robust server-side pagination system using Next.js 14+ and its app router.

The Setup

First, make sure you’re running Next.js 14 or later. If not, let’s get you set up:

npx create-next-app@latest pagination-demo
cd pagination-demo

Choose TypeScript, ESLint, Tailwind CSS, and the App Router when prompted.

Server-Side Pagination Logic

We’ll start by creating a utility function to handle pagination calculations. Create a new file utils/pagination.ts:

export function getPagination(page: number, size: number) {
  const limit = size ? +size : 3;
  const from = page ? page * limit : 0;
  const to = page ? from + size - 1 : size - 1;

  return { from, to };
}

Now, let’s create our API route. In app/api/posts/route.ts:

import { NextResponse } from 'next/server';
import { getPagination } from '@/utils/pagination';

// Simulated database
const posts = Array.from({ length: 100 }, (_, i) => ({
  id: i + 1,
  title: `Post ${i + 1}`,
  content: `This is the content of post ${i + 1}`
}));

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const size = parseInt(searchParams.get('size') || '10');

  const { from, to } = getPagination(page - 1, size);

  const paginatedPosts = posts.slice(from, to + 1);

  return NextResponse.json({
    currentPage: page,
    totalPages: Math.ceil(posts.length / size),
    pageSize: size,
    totalCount: posts.length,
    posts: paginatedPosts,
  });
}

Server Component for Rendering Posts

Create a new file app/posts/page.tsx:

import Link from 'next/link';

async function getPosts(page = 1, size = 10) {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/posts?page=${page}&size=${size}`, { cache: 'no-store' });
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

export default async function Posts({ 
  searchParams 
}: { 
  searchParams: { page?: string, size?: string } 
}) {
  const page = Number(searchParams.page) || 1;
  const size = Number(searchParams.size) || 10;
  const { posts, currentPage, totalPages } = await getPosts(page, size);

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-4">Posts</h1>
      <ul className="space-y-2">
        {posts.map((post: any) => (
          <li key={post.id} className="bg-gray-100 p-2 rounded">
            {post.title}
          </li>
        ))}
      </ul>
      <div className="flex justify-center space-x-2 mt-4">
        {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNumber) => (
          <Link
            key={pageNumber}
            href={`/posts?page=${pageNumber}&size=${size}`}
            className={`px-3 py-1 rounded ${
              currentPage === pageNumber ? 'bg-blue-500 text-white' : 'bg-gray-200'
            }`}
          >
            {pageNumber}
          </Link>
        ))}
      </div>
    </div>
  );
}

Adding Loading and Error States

Create app/posts/loading.tsx:

export default function Loading() {
  return <div>Loading posts...</div>
}

And app/posts/error.tsx:

'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

SEO Optimization

Let’s add some metadata in app/posts/layout.tsx:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Paginated Posts | Your Awesome Blog',
  description: 'Explore our extensive collection of posts with server-side pagination.',
  openGraph: {
    title: 'Paginated Posts | Your Awesome Blog',
    description: 'Explore our extensive collection of posts with server-side pagination.',
    type: 'website',
    url: 'https://yourawesomeblog.com/posts',
  },
}

export default function PostsLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <>{children}</>
}

Performance Optimization

To optimize performance, we can implement Incremental Static Regeneration (ISR). Update your app/posts/page.tsx:

import Link from 'next/link';

async function getPosts(page = 1, size = 10) {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/posts?page=${page}&size=${size}`, { 
    next: { revalidate: 60 } // Revalidate every 60 seconds
  });
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

// ... rest of the component remains the same

Wrapping Up

You’ve now got a slick server-side pagination system using Next.js 14+ app router. Here are the key takeaways:

  1. Server-side pagination reduces client-side load and improves performance.
  2. Next.js 14+ app router makes it easy to create API routes and server components.
  3. Use searchParams in server components to handle query parameters.
  4. Implement loading and error states for better UX.
  5. Leverage ISR for optimal performance with frequently updated content.

For more on Next.js server components and data fetching, check out the official documentation.

Remember, server-side pagination is your friend when dealing with large datasets or when SEO is crucial. It might require more server resources, but it often results in a snappier, more scalable application.

Happy coding, you pagination wizard!

Leave a Reply

Your email address will not be published. Required fields are marked *