A ballot has races, which have candidates, which have finance records, donors, endorsements, and stances. REST would force five round-trips. GraphQL gets it in one.
This is what a typical voter-guide page actually needs in one render pass.
query VoterGuide($addr: String!) {
ballot(address: $addr) {
election { id, date }
races {
office
candidates {
id
name
party
endorsements { org, url }
stances { topic, summary }
finance {
totalRaised
topDonors(limit: 3) { name, amount }
}
}
}
measures {
id
title
yesMeans
noMeans
fiscalImpact
}
}
}GET /v1/ballot?address=...
→ GET /v1/elections/{id}
→ GET /v1/ballots/{id}
→ for each race:
→ GET /v1/candidates/{id}
→ GET /v1/finance/{candidate_id}
→ GET /v1/candidates/{id}/endorsements
→ GET /v1/candidates/{id}/stances
→ for each measure:
→ GET /v1/measures/{id}
// ~40 HTTP requests for a typical municipal ballot.
// GraphQL: 1 request, ~12 KB response.type Query {
ballot(address: String, ocdId: ID): Ballot
jurisdiction(ocdId: ID!): Jurisdiction
candidate(id: ID!): Candidate
measure(id: ID!): Measure
election(id: ID!): Election
}
type Ballot { election, races: [Race!]!, measures: [Measure!]!, pollingPlaces: [PollingPlace!]! }
type Race { office, district, candidates: [Candidate!]!, winner: Candidate }
type Candidate { name, party, bio, finance: Finance, endorsements: [Endorsement!]!, stances: [Stance!]! }
type Measure { title, summary, yesMeans, noMeans, fiscalImpact, supporters, opponents, fullTextUrl }
type Finance { totalRaised, totalSpent, cashOnHand, donors(limit: Int): [Donor!]!, filings: [Filing!]! }
type Officeholder { name, office, jurisdiction, termEnds, contact: Contact }