Mock API
Every code sample and the example app in this repo talk to mock-api/, a
small graphql-yoga server with a
fixed, seeded dataset of a launch company, its rockets, launchpads,
astronauts and ~180 launches. No database, no external network calls.
Why a mock API
Section titled “Why a mock API”sling_gql started as a port of the public SpaceX GraphQL API, which is the
schema sling_gql_gen’s tests were originally verified against. That public
API was down while this PoC was being built, so the project grew its own
server with the same shape of data instead of blocking on someone else’s
uptime. It’s also just more convenient for a PoC: deterministic data,
adjustable latency, and no rate limits.
Run it
Section titled “Run it”cd mock-apinpm installnpm start # or: npm run dev (restarts on file change)Mock GraphQL API ready at http://0.0.0.0:4000/graphqlArtificial latency: 400ms per requestOpen http://localhost:4000/graphql for
GraphiQL. The iOS simulator shares the host network, so the example app
reaches the same localhost:4000 unchanged.
Env vars
Section titled “Env vars”| Var | Default | Meaning |
|---|---|---|
PORT |
4000 |
HTTP port, bound on 0.0.0.0. |
LATENCY_MS |
400 |
Artificial delay awaited once per HTTP request (not per field). |
Turn latency up to actually see skeleton placeholders instead of a flash;
turn it down (LATENCY_MS=0) to make widget tests fast.
Every completed operation is logged to stdout as one line, root fields only:
[14:03:21] query launches, stats (401ms)Example queries
Section titled “Example queries”Cursor-paginated launches, newest first:
{ launches(first: 5) { totalCount pageInfo { hasNextPage endCursor } nodes { flightNumber name date status rocket { name } } }}The same data through offset pagination, filtered and explicitly ordered:
{ launchesPage( limit: 10 offset: 0 filter: { status: SUCCESS, year: 2022 } orderBy: NAME_ASC ) { name status date }}Nested cursor pagination — a rocket’s own launch history:
{ rocket(id: "rocket-falcon") { name launches(first: 5) { totalCount nodes { name date status } } }}Query in full, so you know what’s available:
type Query { company: Company! "Relay-style cursor pagination." launches(first: Int = 20, after: String, filter: LaunchFilter, orderBy: LaunchOrder = DATE_DESC): LaunchConnection! "Classic offset pagination over the same data." launchesPage(limit: Int = 20, offset: Int = 0, filter: LaunchFilter, orderBy: LaunchOrder = DATE_DESC): [Launch!]! launch(id: ID!): Launch nextLaunch: Launch latestLaunch: Launch rockets: [Rocket!]! rocket(id: ID!): Rocket launchpads: [Launchpad!]! astronauts(first: Int = 20, after: String): AstronautConnection! astronaut(id: ID!): Astronaut stats: Stats! "The signed-in user (static in the mock)." me: Viewer!}Viewer and me
Section titled “Viewer and me”me returns a static viewer object (id viewer-1, name Mira Vance, agency
Sling Space, avatarInitials MV). Its field resolvers compute favorites
and favoriteCount lazily from the live in-memory launches array, so they
reflect toggleFavorite mutations within the same server process:
{ me { id name agency avatarInitials favoriteCount favorites { id name date status favorite } }}favoriteCount and favorites are computed at request time, not cached, so
calling toggleFavorite and then re-fetching me will show the updated list.
nextLaunch and latestLaunch are computed relative to real time, not
stored: nextLaunch is the soonest launch with upcoming: true,
latestLaunch the most recent one with upcoming: false. stats
aggregates over every launch (total count, success rate, a per-year
histogram) — it powers the header on the example app’s list screen.
The LaunchFilter input used by both pagination styles:
input LaunchFilter { status: LaunchStatus rocketId: ID year: Int upcoming: Boolean "Case-insensitive substring match on name and details." search: String "Only launches whose favourite flag equals this value." favorite: Boolean}filter: { favorite: true } returns only launches where l.favorite === true (server-side comparison); the example app’s Me tab uses me.favorites
instead, but the filter is there for experiments. The example’s Launches tab
filters on status (filter: { status: SUCCESS }). Because each argument
set differs from the plain launches(first: N) query, the sling_gql cache
stores them as separate entries — coming back to a segment you already
visited is served from cache (zero requests).
Filtering, ordering and pagination run in plain JavaScript over the
in-memory array (resolvers.mjs). Page sizes are clamped to [1, 100]
regardless of what a client asks for. Cursors are base64-encoded offsets
(cursor:<index>) — opaque to clients, easy to read in the resolver code.
Mutations and subscriptions
Section titled “Mutations and subscriptions”toggleFavorite is what the example’s detail-screen heart calls (see
Mutations); favourites live in the server’s memory, so they
persist across app restarts until the server restarts. Subscriptions are wired up in the
schema and resolvers but not yet consumed by the runtime (see
Roadmap):
mutation { toggleFavorite(launchId: "launch-1") { id favorite }}subscription { launchScheduled { id name status }}The other two mutations are scheduleLaunch(input: ScheduleLaunchInput!) and
updateLaunchStatus(id: ID!, status: LaunchStatus!); the other subscription
is launchStatusChanged. Both mutations that change a launch also publish to
the matching subscription topic through graphql-yoga’s createPubSub.
The dataset
Section titled “The dataset”Generated once at process start from a seeded PRNG (mulberry32, seed
0xc0ffee) in data.mjs, so ids and content are stable across restarts:
- 1 company, 5 rockets (one per
RocketFamily), 4 launchpads, 24 astronauts - ~181 launches spread from 2010 through a few years out, of which about 15
are always in the future (
upcoming: true, statusSCHEDULEDorSCRUBBED)
The only thing that changes between restarts is which launches count as past
vs. upcoming, since that’s computed relative to Date.now() rather than
stored — everything else is deterministic.
Regenerating the introspection snapshot
Section titled “Regenerating the introspection snapshot”The example app’s graphql/schema.json — the input to sling_gql_gen — is
a snapshot, not fetched live:
cd mock-api && npm run introspectThis imports the executable schema directly (no server needs to be running)
and overwrites ../example/graphql/schema.json. Run it any time
schema.graphql changes.
Evolving the schema
Section titled “Evolving the schema”-
Edit
mock-api/schema.graphql. It’s the one source of truth for shape — add the field, type, or resolver logic (resolvers.mjs) you need. -
Re-run introspection to refresh
example/graphql/schema.json:Terminal window cd mock-api && npm run introspect -
Regenerate the Dart accessors — see Code generation:
Terminal window cd exampledart run ../packages/sling_gql_gen/bin/sling_gql_gen.dart \--schema graphql/schema.json --out lib/generated/schema.dart -
Re-run the example’s widget tests, which hit the running server and assert exact request counts — see Example app.