At scale, the difference between a well-optimised API integration and a naive one can mean the difference between sub-second dashboards and timeouts, or staying within rate limits versus being blocked. This guide covers the patterns that matter most when building against the Lumar GraphQL API. ## Pagination page size All connection fields accept a `first` or `last` argument between 1 and 1,000. Choosing the right page size depends on your use case: | Use case | Recommended page size | | ------------------------ | --------------------- | | UI rendering | 20–100 | | Data export / processing | 100–500 | Always use `pageInfo.hasNextPage` combined with `endCursor` to drive iteration. Do not guess at offsets or rely on `totalCount` alone to calculate page boundaries. Here is a complete example of iterating through all projects in an account: ```graphql query GetAllProjects($accountId: ObjectID!, $after: String) { getAccount(id: $accountId) { projects(first: 100, after: $after) { nodes { id name } pageInfo { hasNextPage endCursor } totalCount } } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0" } ``` **Response:** ```json { "data": { "getAccount": { "projects": { "nodes": [ { "id": "TjAwN1Byb2plY3Q2MTMy", "name": "Main website" }, { "id": "TjAwN1Byb2plY3Q2MTMz", "name": "Blog" } ], "pageInfo": { "hasNextPage": true, "endCursor": "Mg" }, "totalCount": 47 } } } } ``` Loop through all pages in TypeScript: ```typescript async function fetchAllProjects(accountId: string): Promise { const allProjects: Project[] = []; let after: string | null = null; let hasNextPage = true; while (hasNextPage) { const result = await executeQuery(GET_ALL_PROJECTS, { accountId, after }); const { nodes, pageInfo } = result.data.getAccount.projects; allProjects.push(...nodes); hasNextPage = pageInfo.hasNextPage; after = pageInfo.endCursor; } return allProjects; } ``` :::note Fetch `totalCount` on the first page only — omit it from subsequent requests to avoid the extra computation on every page. ::: ## Prefer `nodes` over `edges` when you don't need cursors GraphQL connections expose two equivalent ways to access items. `nodes` is shorthand for `edges.node` and produces cleaner, less verbose queries. Use it by default. ```graphql # Simpler — use when you don't need per-item cursors nodes { id name } # Use when you need individual item cursors edges { cursor node { id name } } ``` Only reach for `edges` when you need the per-item `cursor` value — for example, when resuming a partially-completed export from a specific position in a large result set. ## Batching multiple operations The API supports request batching — multiple GraphQL operations sent in a single HTTP request as a JSON array. This reduces round trips and TCP connection overhead when you have several independent operations to fire at once. When using Apollo Client, configure `BatchHttpLink`: ```typescript const batchLink = new BatchHttpLink({ uri: "https://api.lumar.io/graphql", batchMax: 25, // up to 25 operations per request batchInterval: 20, // wait 20ms to collect operations before sending }); ``` :::note The batched request body is a JSON array. The Lumar API supports this format natively — no extra configuration is required on the server side. ::: Good use cases for batching: - Creating or deleting many alerts in a single user action. - Fetching multiple independent resources (e.g. several crawl statuses) in parallel without composing a multi-root query. ## Polling for live crawl status When monitoring an active crawl, poll at intervals appropriate to the current state rather than opening a persistent connection: | State | Poll interval | | ---------------------------- | ---------------- | | Active crawl | every 5 seconds | | Idle / background monitoring | every 60 seconds | Always stop polling on error to avoid hammering a failing endpoint. ```typescript // Apollo Client example const { startPolling, stopPolling } = useQuery(GET_CRAWL_STATUS, { variables: { crawlId }, onError: () => stopPolling(), }); useEffect(() => { if (crawlIsActive) { startPolling(5000); // 5s while crawl is running } else { stopPolling(); } return () => stopPolling(); }, [crawlIsActive]); ``` :::warning Do not poll at short intervals (under 5 seconds) for non-critical status checks. Sustained high-frequency polling across multiple crawls can push you toward the [rate limit](rate-limits) of 6,000 requests per 5-minute window. ::: ## Reduce round trips with multi-root queries GraphQL allows multiple root fields in a single query. Instead of making three separate HTTP requests for account information, a project list, and reference data, combine them: ```graphql query DashboardBootstrap($accountId: ObjectID!) { getAccount(id: $accountId) { name subscription { plan { name } } projects(first: 20) { nodes { id name primaryDomain } totalCount } } getReportTemplates { nodes { id name } } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0" } ``` **Response:** ```json { "data": { "getAccount": { "name": "Acme Corp", "subscription": { "plan": { "name": "Professional" } }, "projects": { "nodes": [ { "id": "TjAwN1Byb2plY3Q2MTMy", "name": "Main website", "primaryDomain": "https://example.com" } ], "totalCount": 12 } }, "getReportTemplates": { "nodes": [{ "id": "TjAwOFJlcG9ydFRlbXBsYXRlMQ", "name": "SEO Overview" }] } } } ``` This pattern is especially valuable on application startup or page load, where latency is most noticeable. :::tip See the [Advanced Query Patterns](advanced-query-patterns) page for more examples of composing efficient multi-root queries. :::