Skip to content

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.

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 the first version 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 tests and demos: deterministic data, adjustable latency, and no rate limits.

mock-api/
cd mock-api
bun install
bun start # or: bun run dev (restarts on file change)
Mock GraphQL API ready at http://0.0.0.0:4000/graphql
Artificial latency: 400ms per request (override: x-mock-latency-ms header)

Open http://localhost:4000/graphql for GraphiQL. The iOS simulator shares the host network, so the example app reaches the same localhost:4000 unchanged; the Android emulator reaches it at 10.0.2.2:4000.

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).
SEQUENCE_MS 4000 Launch sequence step after scheduleLaunch (IN_FLIGHT, then SUCCESS/FAILURE).

Turn latency up to actually see skeleton placeholders instead of a flash; turn it down (LATENCY_MS=0) to make widget tests fast.

A single request can override it with an x-mock-latency-ms header (milliseconds; 0 for none, an invalid value falls back to LATENCY_MS). It is per request rather than server state, so clients and parallel tests never see each other’s setting. The example app’s latency picker sends it through SlingClient(transport:) — see Example app.

Every completed operation is logged to stdout as one line, root fields only:

[14:03:21] query launches, stats (401ms)

The same API also runs in the browser, with no server: that is what Try it live and the demos in the guides use. bun run build:browser builds it for the example’s web version.

graphql-http-server.mjs serves the same schema and resolvers through another implementation: graphql-http (the GraphQL-over-HTTP reference server) for queries and mutations, graphql-sse for subscriptions, on plain node:http (PORT=4001 node graphql-http-server.mjs). melos run test:example:graphql-http runs the example’s end-to-end tests against it — batching, aliases, fragments, mutations, partial errors and SSE subscriptions — so the client is checked against a server it was not developed against.

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:

mock-api/schema.graphql
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!
"Launches, rockets and astronauts whose name contains `text`."
search(text: String!, first: Int = 20): [SearchResult!]!
"Any launch, rocket, launchpad or astronaut by id."
node(id: ID!): Node
}

Launch, Rocket, Launchpad and Astronaut implement interface Node { id: ID! }; union SearchResult = Launch | Rocket | Astronaut. search(text:) matches names case-insensitively (launches newest first, then rockets, then astronauts; the example’s Search tab); node(id:) resolves any of the four by id. Both resolve the concrete type from the id prefix (launch-, rocket-, pad-, astro-):

{
search(text: "al") {
__typename
... on Launch { id name date }
... on Rocket { id name stages }
... on Astronaut { id name agency }
}
}

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:

mock-api/schema.graphql
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.

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 served over Server-Sent Events on the same endpoint (POST with accept: text/event-stream, which is what the runtime sends — see Subscriptions); the example’s list screen keeps launchStatusChanged open, so running updateLaunchStatus in GraphiQL updates its rows:

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.

scheduleLaunch also starts a launch sequence the server runs by itself: the launch goes IN_FLIGHT after SEQUENCE_MS (default 4000 ms), then SUCCESS — or FAILURE if its name contains “fail” — after another SEQUENCE_MS, each step published on launchStatusChanged. That is what the example’s mission-control flow watches; its tests run the server with SEQUENCE_MS=700.

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, status SCHEDULED or SCRUBBED)

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.

The example app’s graphql/schema.json — the input to sling_gql_gen — is a snapshot, not fetched live:

Terminal window
cd mock-api && bun run introspect

This imports the executable schema directly (no server needs to be running) and overwrites ../example/graphql/schema.json. Run it any time schema.graphql changes.

  1. 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.

  2. Re-run introspection to refresh example/graphql/schema.json:

    Terminal window
    cd mock-api && bun run introspect
  3. Regenerate the Dart accessors — see Code generation:

    Terminal window
    cd example
    dart run ../packages/sling_gql_gen/bin/sling_gql_gen.dart \
    --schema graphql/schema.json --out lib/generated/schema.dart
  4. Re-run the example’s widget tests, which hit the running server and assert exact request counts — see Example app.