Dev notes
A Pokémon browser on PokeAPI's public Hasura endpoint — plain fetch over Apollo, server-side initial data, and useInfiniteQuery pagination.
The PokeAPI REST endpoint returns a massive JSON blob per Pokémon — game versions, form descriptions, encounter data — most of which a card view ignores. GraphQL lets you ask for exactly what the card needs. The PokeAPI v2 endpoint is powered by Hasura, which introspects the Postgres database and auto-generates the entire schema. Every table and relationship becomes queryable with filtering, sorting, aggregates, and nested joins — no resolvers written by hand.
Apollo Client adds around 60kb gzipped to the client bundle. GraphQL is just HTTP — a POST request with a JSON body containing query and variables. Plain fetch handles that in about 10 lines. A client library earns its cost when you need a normalized cache, optimistic mutations, or real-time subscriptions. None of those are needed here, so the library would just be weight.
The query string itself is a constant that never changes — only the variables object changes with user input. This means the network tab always shows the same query shape, which makes debugging easier and lets Hasura cache the parsed query on its side. Interpolation also opens the door to injection: someone passing }) { id } query Anything { as a search string could restructure the query entirely. Variables are always treated as scalars, never as query syntax.
Page 1 is fetched server-side via a fetchPokemonDirect function that calls PokeAPI straight from the server — no proxy needed, since server code doesn't have CSP constraints. It passes next: { revalidate: 3600 } to the underlying fetch so repeated renders within an hour hit Next.js's data cache. The page wraps the server component in a Suspense boundary with a skeleton fallback — the skeleton streams immediately while the fetch resolves, then the real grid drops in.
The initialData prop goes straight into the useInfiniteQuery cache as the first page. TanStack treats it as fresh for 30 seconds and skips the initial fetch. The seed only applies to the no-filter key — passing it unconditionally caused a bug where filtered queries got seeded with unfiltered server data and the 30-second stale time prevented the filter fetch from firing.
The original implementation used manual loadedKey, filterKey, hasServerData, abortRef, and offset state. After converting to useInfiniteQuery all of that is gone. The query key includes debouncedName and activeType — when either changes, TanStack cancels the in-flight request and fires a fresh fetch automatically.
The /api/graphql proxy route forwards the POST body upstream to beta.pokeapi.co/graphql/v1beta. It exists because the app's CSP locks connect-src to same-origin, so the browser can't reach the upstream URL directly. Secondary benefit: the upstream URL stays out of the client bundle entirely.