docs / grandstack-web / Concepts

Architecture of grandstack-web

grandstack-web is a single-page React app that talks to grandstack-server over GraphQL. It is a movie and people search, filter, compare and edit app, written to show what a GraphQL-on-Neo4j front end looks like and to be adapted to your own data.

Applies to grandstack-web v0.5.1 (React 19, Vite 8, urql 5, GraphQL Code Generator, TypeScript).

Stack

ConcernChoiceNotes
UIReact 19 + react-router 8Client-side routes; nginx falls back to index.html.
GraphQL clienturql 5 with cacheExchange and fetchExchangeDefault request policy is cache-and-network.
TypesGraphQL Code Generator, client presetOperations written with the graphql() tag are typed from the live schema.
AnimationmotionRespects prefers-reduced-motion.
Graph viewreact-force-graph-2d (MIT)Neo4j’s own NVL has a custom licence, so it is not used.
StylingPlain CSS with design tokenssrc/theme/tokens.css, styled after Neo4j’s Needle design system (see customising).
Posters, avatarsGenerated in the browserNo image files.

Folder map

src/
  main.tsx                 urql client, router, motion config
  App.tsx                  routes, header, Add menu, compare tray, theme toggle
  pages/                   one component per route (Home, Results, MovieDetail, MovieNew, MovieEdit,
                           People, PersonDetail, PersonNew, PersonEdit, Compare)
  search/                  FilterSidebar, MovieCard, PersonCard
  forms/                   MovieForm, PersonForm, PersonPicker, DeleteButtons, and model.ts (pure logic)
  graph/                   GraphView (shared), MovieGraph, PersonGraph
  lib/                     filters.ts, peopleFilters.ts (URL state and GraphQL where/sort), compare.tsx,
                           hash.ts and initials.ts (small helpers used by posters and avatars)
  poster/Poster.tsx        generated poster art
  ui/                      Button, Chip, Checkbox, RatingBadge, Skeleton, Avatar, ConfirmDialog, PublicEditBanner
  theme/tokens.css         colours, spacing, radii, light and dark
  styles.css               all component styles
  graphql/generated/       committed output of codegen (do not edit)
e2e/                       browser end-to-end checks (see e2e/README.md)
codegen.ts                 where the schema comes from and which files hold operations
nginx.conf, Dockerfile     production image

Routes

PathPagePurpose
/HomeHero search, genre chips, top rated, newest, most prolific actors and directors
/moviesResultsFaceted search with filters in the URL
/movies/newMovieNewCreate a movie
/movies/:titleMovieDetailFacts, cast, graph, recommendations
/movies/:title/editMovieEditEdit or delete a movie
/peoplePeopleBrowse people by role
/people/newPersonNewCreate a person
/people/:namePersonDetailStats, filmography, collaborators, graph
/people/:name/editPersonEditEdit or delete a person
/compareCompareUp to four movies side by side
/newredirectGoes to /movies/new

Titles and names are the keys, so they appear URL-encoded in paths (/people/Timoth%C3%A9e%20Chalamet).

Data flow

State lives in the URL, not in component state, for anything a user might want to share or reload:

In grandstack-web the URL query string is parsed into a Filters object, turned into typed GraphQL variables, sent with urql, rendered, and any filter change is written back to the URL.

Figure: one loop from the URL to the screen and back. Click the image for full size; the numbered notes below carry the same information as text.

  1. The URL is the source of truth. Filters live in the query string (g, d, r, rt, p, s, q), so results are shareable, reloadable and work with the back button.
  2. Pure functions in the middle. parseFilters, buildWhere and buildSort turn the query string into the where and sort variables; they are unit-tested. buildWhere can leave one facet out, which is how facet counts work.
  3. Typed documents. Every operation is written with graphql() and typed by npm run codegen against the live schema, so a wrong field fails at codegen or tsc.
  4. Components only render and emit events. A changed filter calls serializeFilters and setSearchParams, which closes the loop. Text inputs keep local state and debounce the URL update, otherwise fast typing drops characters.
  5. Almost nothing else is stored. Only the compare list and the theme are kept in localStorage.
  • src/lib/filters.ts owns the movie filters (q, g genres, d decades, r minimum rating, rt maximum runtime, p person, s sort). src/lib/peopleFilters.ts owns the people filters (q, role, mf minimum films, s). Both are pure functions with unit tests.
  • Text inputs that write to the URL keep local state and debounce the URL update (see People.tsx). Binding an input straight to the URL drops characters when typing fast, because the URL update is asynchronous.
  • The compare selection (localStorage key grandstack.compare, at most four titles) and the theme (grandstack.theme) are the only browser-stored state.
  • Every GraphQL document is written next to the component that uses it with the graphql() tag from src/graphql/generated. Codegen validates it against the live schema and generates the result types, so a typo in a field name fails at codegen time and a wrong use of a result field fails at tsc.

Caching and freshness

The urql client uses requestPolicy: "cache-and-network": pages show cached data immediately and always refetch, so lists and counts are fresh after an edit without wiring cache invalidation by hand. Edit forms use network-only so they never start from stale data. The server refreshes stored film counts after every mutation, so a refetch after a save is correct.

Build and deploy

npm run build type-checks and builds static files with Vite. The Dockerfile builds them and serves them with unprivileged nginx on port 8080, with long-lived caching for /assets/ and a fallback to index.html for client routes.

VITE_GRAPHQL_URL is baked in at build time (a build argument in the Dockerfile, default http://localhost:4000/graphql). It must be a URL the browser can reach, not a container-internal hostname. The server’s CORS_ORIGIN must allow the web origin.

Next: features, customising, recipes, testing.

Source: grandstack-web/docs/architecture.md (this page is a copy; edit it in the repository)