Cursor API Integration
Purpose: Integrate cursors with REST and GraphQL APIs
Last Updated: 2024-11-26
Overview
This guide covers patterns for exposing cursor-based pagination through API endpoints.
For complete code examples, see Cursor Pagination Examples.
REST API Patterns
Basic Endpoint
See: Example 3 - REST API Endpoint
// GET /api/nodes?cursor=...&limit=50
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const cursor = searchParams.get('cursor') ?? undefined;
const limit = Math.min(
parseInt(searchParams.get('limit') ?? '50'),
100 // Max limit
);
const result = await paginateNodes(cursor, limit);
return Response.json({
data: result.nodes,
pagination: {
cursor: result.cursor,
hasMore: result.hasMore,
limit
}
});
}
Response Format
{
"data": [
{ "id": "123", "name": "Node 1" },
{ "id": "124", "name": "Node 2" }
],
"pagination": {
"cursor": "eyJsYXN0SWQiOjEyNCwibGltaXQiOjUwfQ==",
"hasMore": true,
"limit": 50
}
}
Error Handling
export async function GET(request: Request) {
try {
const cursor = searchParams.get('cursor') ?? undefined;
const result = await paginateNodes(cursor);
return Response.json({ data: result.nodes, ...result });
} catch (error) {
if (error.message === 'Invalid cursor') {
return Response.json(
{ error: 'Invalid cursor, please start from the beginning' },
{ status: 400 }
);
}
throw error;
}
}
GraphQL Patterns
Relay Connection Pattern
See: Example 4 - GraphQL Resolver
const resolvers = {
Query: {
nodes: async (_parent, { first, after }, { client }) => {
const result = await paginateNodes(client, after);
return {
edges: result.nodes.map(node => ({
node,
cursor: encodeCursor({
lastId: node.id,
limit: first,
direction: 'forward',
timestamp: Date.now(),
version: 1
})
})),
pageInfo: {
hasNextPage: result.hasMore,
hasPreviousPage: false,
startCursor: result.nodes[0]
? encodeCursor({ lastId: result.nodes[0].id })
: null,
endCursor: result.cursor
}
};
}
}
};
GraphQL Schema
type Query {
nodes(
first: Int = 50
after: String
): NodeConnection!
}
type NodeConnection {
edges: [NodeEdge!]!
pageInfo: PageInfo!
}
type NodeEdge {
node: Node!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Client Usage
REST Client
async function fetchAllNodes() {
let cursor: string | null = null;
let allNodes: Node[] = [];
do {
const url = new URL('/api/nodes', baseUrl);
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url);
const { data, pagination } = await response.json();
allNodes.push(...data);
cursor = pagination.hasMore ? pagination.cursor : null;
} while (cursor);
return allNodes;
}
GraphQL Client
const query = gql`
query GetNodes($after: String) {
nodes(first: 50, after: $after) {
edges {
node { id name }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
async function fetchAllNodes() {
let after: string | null = null;
let allNodes: Node[] = [];
do {
const { data } = await client.query({
query,
variables: { after }
});
allNodes.push(...data.nodes.edges.map(e => e.node));
after = data.nodes.pageInfo.hasNextPage
? data.nodes.pageInfo.endCursor
: null;
} while (after);
return allNodes;
}
Infinite Scroll
React Example
function useInfiniteNodes() {
const [nodes, setNodes] = useState<Node[]>([]);
const [cursor, setCursor] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const loadMore = async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const url = `/api/nodes${cursor ? `?cursor=${cursor}` : ''}`;
const response = await fetch(url);
const { data, pagination } = await response.json();
setNodes(prev => [...prev, ...data]);
setCursor(pagination.cursor);
setHasMore(pagination.hasMore);
} finally {
setLoading(false);
}
};
return { nodes, loading, hasMore, loadMore };
}
Best Practices
✅ Do
- Validate cursor before using
- Set maximum page size limits
- Include pagination metadata in responses
- Handle invalid cursors gracefully
- Document cursor format for clients
- Use consistent response structure
❌ Don't
- Expose internal cursor structure
- Allow unbounded page sizes
- Assume cursor is always valid
- Return errors without fallback
- Skip cursor expiration checks
Related Documentation
- Implementation Guide - Cursor implementation patterns
- Performance Guide - Optimization strategies
- Examples - Complete code examples
Next steps: See Examples for complete implementation code.