Files
gh-djankies-claude-configs-…/skills/implementing-query-pagination/references/api-implementation-examples.md
2025-11-29 18:22:25 +08:00

3.2 KiB

Complete API Implementation Examples

Example 1: API Endpoint with Cursor Pagination

import { prisma } from './prisma-client';

type GetPostsParams = {
  cursor?: string;
  limit?: number;
};

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const cursor = searchParams.get('cursor') || undefined;
  const limit = Number(searchParams.get('limit')) || 20;

  if (limit > 100) {
    return Response.json(
      { error: 'Limit cannot exceed 100' },
      { status: 400 }
    );
  }

  const posts = await prisma.post.findMany({
    take: limit,
    skip: cursor ? 1 : 0,
    cursor: cursor ? { id: cursor } : undefined,
    orderBy: { createdAt: 'desc' },
    include: {
      author: {
        select: { id: true, name: true, email: true },
      },
    },
  });

  const nextCursor = posts.length === limit
    ? posts[posts.length - 1].id
    : null;

  return Response.json({
    data: posts,
    nextCursor,
    hasMore: nextCursor !== null,
  });
}

Client usage:

async function loadMorePosts() {
  const response = await fetch(`/api/posts?cursor=${nextCursor}&limit=20`);
  const { data, nextCursor: newCursor, hasMore } = await response.json();

  setPosts(prev => [...prev, ...data]);
  setNextCursor(newCursor);
  setHasMore(hasMore);
}

Example 2: Filtered Cursor Pagination

type GetFilteredPostsParams = {
  cursor?: string;
  authorId?: string;
  tag?: string;
  limit?: number;
};

async function getFilteredPosts({
  cursor,
  authorId,
  tag,
  limit = 20,
}: GetFilteredPostsParams) {
  const where = {
    ...(authorId && { authorId }),
    ...(tag && { tags: { some: { name: tag } } }),
  };

  const posts = await prisma.post.findMany({
    where,
    take: limit,
    skip: cursor ? 1 : 0,
    cursor: cursor ? { id: cursor } : undefined,
    orderBy: { createdAt: 'desc' },
  });

  return {
    data: posts,
    nextCursor: posts.length === limit ? posts[posts.length - 1].id : null,
  };
}

Index requirement:

model Post {
  id        String   @id @default(cuid())
  authorId  String
  createdAt DateTime @default(now())

  @@index([authorId, createdAt, id])
}

Example 3: Small Admin Table with Offset

type GetAdminUsersParams = {
  page?: number;
  pageSize?: number;
  search?: string;
};

async function getAdminUsers({
  page = 1,
  pageSize = 50,
  search,
}: GetAdminUsersParams) {
  const skip = (page - 1) * pageSize;

  const where = search
    ? {
        OR: [
          { email: { contains: search, mode: 'insensitive' as const } },
          { name: { contains: search, mode: 'insensitive' as const } },
        ],
      }
    : {};

  const [users, total] = await Promise.all([
    prisma.user.findMany({
      where,
      skip,
      take: pageSize,
      orderBy: { createdAt: 'desc' },
      select: {
        id: true,
        email: true,
        name: true,
        role: true,
        createdAt: true,
      },
    }),
    prisma.user.count({ where }),
  ]);

  return {
    data: users,
    pagination: {
      page,
      pageSize,
      totalPages: Math.ceil(total / pageSize),
      totalRecords: total,
      hasNext: page < Math.ceil(total / pageSize),
      hasPrev: page > 1,
    },
  };
}