Skip to main content

Pagination Patterns

The template provides two complementary pagination utilities: a validation layer for API routes (lib/utils/pagination-validation.ts) and a metadata helper for content pagination (lib/paginate.ts). Together they cover server-side parameter validation and client-side page calculation.

Pagination Validation (API Routes)

The pagination-validation module provides shared validation logic for pagination parameters across admin API routes.

Types

interface PaginationParams {
page: number;
limit: number;
}

interface PaginationError {
error: string;
status: 400;
}

PaginationParams represents successfully validated parameters. PaginationError is returned when validation fails, always with HTTP status 400.

validatePaginationParams

The main validation function extracts and validates page and limit from URL search params:

import { validatePaginationParams } from '@/lib/utils/pagination-validation';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const result = validatePaginationParams(searchParams);

// Check if validation failed
if ('error' in result) {
return Response.json(
{ error: result.error },
{ status: result.status }
);
}

// Use validated params
const { page, limit } = result;
const offset = (page - 1) * limit;

const data = await fetchItems({ offset, limit });
return Response.json(data);
}

Validation Rules

ParameterDefaultValidation
page1Must be a positive integer (1 or greater)
limit10Must be between 1 and 100 inclusive

When a parameter is missing from the URL, the default value is used. When a parameter is present but invalid, the function returns a PaginationError.

Full Implementation

export function validatePaginationParams(
searchParams: URLSearchParams
): PaginationParams | PaginationError {
const pageParam = searchParams.get('page');
const limitParam = searchParams.get('limit');

const page = pageParam ? parseInt(pageParam, 10) : 1;
const limit = limitParam ? parseInt(limitParam, 10) : 10;

if (isNaN(page) || page < 1) {
return {
error: 'Invalid page parameter. Must be a positive integer.',
status: 400,
};
}

if (isNaN(limit) || limit < 1 || limit > 100) {
return {
error: 'Invalid limit parameter. Must be between 1 and 100.',
status: 400,
};
}

return { page, limit };
}

Discriminated Union Pattern

The return type uses a discriminated union, letting you narrow the result by checking for the error property:

const result = validatePaginationParams(searchParams);

if ('error' in result) {
// result is PaginationError
return Response.json({ error: result.error }, { status: result.status });
}

// result is PaginationParams
const { page, limit } = result;

This pattern avoids throwing exceptions for validation failures, keeping the control flow explicit.

Content Pagination (Page Metadata)

The lib/paginate.ts module provides lightweight helpers for calculating pagination metadata on content listings.

Constants

const PER_PAGE = 12;

The default items-per-page value used across the application for content listings.

totalPages

Calculates the total number of pages given a collection size:

import { totalPages, PER_PAGE } from '@/lib/paginate';

const total = totalPages(items.length); // e.g., 37 items -> 4 pages
const customTotal = totalPages(items.length, 20); // 37 items, 20 per page -> 2 pages

Implementation:

export function totalPages(size: number, perPage: number = PER_PAGE) {
return Math.ceil(size / perPage);
}

paginateMeta

Computes the page number and start index for array slicing:

import { paginateMeta, PER_PAGE } from '@/lib/paginate';

const { page, start } = paginateMeta(3); // page=3, start=24
const items = allItems.slice(start, start + PER_PAGE);

The function accepts both string and number page values, handling the URL search param case where page is a string:

export function paginateMeta(
rawPage: number | string = 1,
perPage: number = PER_PAGE
) {
const page = typeof rawPage === 'string' ? parseInt(rawPage) : rawPage;
const start = (page - 1) * perPage;
return { page, start };
}

Usage Patterns

API Route with Validation

A typical admin API route uses validatePaginationParams for safe parameter extraction:

import { validatePaginationParams } from '@/lib/utils/pagination-validation';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const pagination = validatePaginationParams(searchParams);

if ('error' in pagination) {
return NextResponse.json(
{ error: pagination.error },
{ status: pagination.status }
);
}

const { page, limit } = pagination;
const offset = (page - 1) * limit;

const [items, total] = await Promise.all([
db.query.items.findMany({ offset, limit }),
db.query.items.count(),
]);

return NextResponse.json({
data: items,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}

Content Listing Page

For server-rendered content pages, use paginateMeta and totalPages:

import { paginateMeta, totalPages, PER_PAGE } from '@/lib/paginate';

interface PageProps {
searchParams: { page?: string };
}

export default async function ListingPage({ searchParams }: PageProps) {
const { page, start } = paginateMeta(searchParams.page);
const allItems = await getItems();
const pageItems = allItems.slice(start, start + PER_PAGE);
const pages = totalPages(allItems.length);

return (
<div>
<ItemGrid items={pageItems} />
<Pagination currentPage={page} totalPages={pages} />
</div>
);
}

Combining Both Utilities

For API endpoints that serve paginated content, you can combine both utilities:

import { validatePaginationParams } from '@/lib/utils/pagination-validation';
import { totalPages } from '@/lib/paginate';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const result = validatePaginationParams(searchParams);

if ('error' in result) {
return Response.json(
{ error: result.error },
{ status: result.status }
);
}

const items = await getAllItems();
const { page, limit } = result;
const start = (page - 1) * limit;
const pageItems = items.slice(start, start + limit);

return Response.json({
data: pageItems,
meta: {
page,
limit,
total: items.length,
totalPages: totalPages(items.length, limit),
},
});
}

Source Files

FilePurpose
lib/utils/pagination-validation.tsAPI pagination parameter validation
lib/paginate.tsContent pagination metadata helpers