How Nitter Handles Cursor-Based Pagination in Its API Requests

Nitter implements cursor-based pagination by extracting cursor tokens from HTTP query parameters, injecting them into Twitter GraphQL requests via the cursorParam helper in src/api.nim, and propagating the returned next_cursor from JSON responses into HTML "Show more" links rendered by src/views/timeline.nim.

The open-source Twitter alternative front-end zedeus/nitter relies on cursor-based pagination to fetch timeline data while maintaining stateless HTTP interactions. This mechanism allows Nitter to request successive slices of tweets, replies, and media without exposing Twitter's internal API limits. The implementation spans multiple Nim modules that handle parameter extraction, GraphQL variable construction, and response parsing.

The Cursor Pagination Pipeline

Extracting Cursor Parameters from Requests

In src/routes/router_utils.nim, Nitter reads incoming cursor values from the cursor query parameter or falls back to the legacy max_position parameter. The code retrieves and URL-decodes the string before passing it downstream to timeline route handlers.

According to the source code, the extraction logic appears as:

let cursor = req.params.getOrDefault("cursor")
let decoded = decodeUrl(if cursor.len > 0: cursor else: @"max_position", false)

Encoding Cursors for GraphQL Variables

The cursorParam procedure in src/api.nim safely JSON-escapes the cursor value and injects it into GraphQL variables. This helper ensures the cursor string is properly formatted as "cursor": "<encoded-value>" within the request payload sent to Twitter's API endpoints.

Endpoint-Specific URL Construction

Depending on the timeline type—tweets, replies, media, or articles—src/api.nim selects the appropriate GraphQL endpoint. Functions including userTweetsUrl, userTweetsAndRepliesUrl, mediaUrl, and userArticlesUrl all accept the cursor string as a parameter and embed it into the API request structure.

For example, userTweetsUrl builds the request as follows:

proc userTweetsUrl(id: string; cursor: string): ApiReq =
  # `cursorParam` injects `"cursor": "<value>"` into the variables

  return apiReq(graphUserTweetsV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles)

Parsing Next Cursors from Responses

After fetching data, src/parser.nim locates continuation tokens within the GraphQL JSON response. The parser searches for fields named cursor-bottom, cursor-showmore, or next_cursor, storing these values in result objects such as result.bottom or result.thread.cursor for subsequent use.

Rendering Pagination Controls

In src/views/timeline.nim, the renderMore procedure constructs the "Show more" hyperlink. This function URL-encodes the cursor and appends it to the request URL as a cursor= query parameter, completing the cycle when users click to load the next page.

The view implementation appears as:

proc renderMore*(query: Query; cursor: string; focus = ""; extra = ""): VNode =
  a(href = &"?{extra}{getQuery(query)}cursor={encodeUrl(cursor, usePlus = false)}{focus}") :
    # link text …

Key Source Files

The cursor-based pagination system spans the following files in the repository:

  • src/api.nim – Defines cursorParam, builds GraphQL URLs for each timeline type (tweets, replies, media, etc.)
  • src/routes/router_utils.nim – Retrieves and decodes the cursor query parameter from HTTP requests
  • src/routes/timeline.nim – Calls the appropriate GraphQL function with the cursor and passes the result to the view
  • src/views/timeline.nim – Renders the "more" link that carries the cursor forward
  • src/parser.nim – Parses GraphQL responses, extracts next_cursor / cursor-bottom values
  • src/parserutils.nim – Helper for reading cursor fields from JSON structures

Relevant lines in the codebase include:

Summary

  • Nitter uses Twitter's GraphQL cursor mechanism to page through timeline data including tweets, replies, and media.
  • The cursor parameter is extracted from HTTP requests in router_utils.nim and decoded before processing.
  • The cursorParam helper in api.nim safely JSON-escapes cursors and injects them into GraphQL variables for each endpoint type.
  • Response parsers in parser.nim locate next_cursor, cursor-bottom, or cursor-showmore fields to identify subsequent pages.
  • The view layer in views/timeline.nim renders "Show more" links that encode the cursor into the URL, enabling seamless pagination without exposing Twitter's raw API.

Frequently Asked Questions

How does Nitter handle the initial page request when no cursor exists?

When no cursor parameter is present in the URL, Nitter treats the request as an initial page load. The router_utils.nim logic uses an empty string or default value, which the GraphQL functions in api.nim handle by omitting the cursor from the variables, causing Twitter's API to return the first page of results.

What is the difference between cursor and max_position parameters?

The cursor parameter is the modern query string key used for pagination, while max_position serves as a legacy fallback parameter supported for backward compatibility. The extraction logic in src/routes/router_utils.nim checks for cursor first, then falls back to max_position if the former is absent.

How does Nitter determine when to show the "Show more" button?

The parser in src/parser.nim extracts continuation tokens like cursor-bottom or next_cursor from the GraphQL response. If these fields contain values, the view layer passes them to renderMore in src/views/timeline.nim, which generates the navigation link. Absence of these tokens indicates the final page.

Is cursor encoding necessary for the GraphQL requests?

Yes, cursor encoding is essential because Twitter's GraphQL variables require properly escaped JSON strings. The cursorParam function in src/api.nim ensures special characters within cursor tokens do not break the JSON structure when injected into requests via functions like userTweetsUrl or mediaUrl.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →