REST vs GraphQL on the platform: practical choices that scale
Deciding between REST and GraphQL isn't theology; it's economics. On our full-stack app generator (a serious Bubble alternative), both are first-class, code-gen ready, and wired into observability. Here's how I pick with enterprise teams building customer portals, partner APIs, and a billing and invoicing module AI.
When REST wins
- Stable, transactional flows: Payments, refunds, and invoice issuance map cleanly to verbs. POST /invoices, PATCH /invoices/{id}/status keeps audit trails simple.
- Compliance and caching: Versioned routes (v1, v2) plus immutable GETs play nicely with CDNs and WAFs. PCI/SOC reviews also prefer explicit surfaces.
- Rate limiting and SLAs: Per-route quotas are predictable for partners; you can throttle POST /payments harder than GET /plans.
- Webhooks: Event fan-out (invoice.paid) is simpler when the consumer already speaks REST.
When GraphQL shines
- Aggregated dashboards: Finance ops need invoices, customers, balances, and dunning status in one round trip. A single query trims mobile latency.
- Schema evolution: Add fields without versioning explosions; deprecate gradually while clients opt in.
- AI assistants: The billing and invoicing module AI can request exactly the fields it needs, reducing token bloat and hallucination surface.
- Internal micro-frontends: Teams can move fast behind a typed, discoverable graph while backend boundaries stay intact.
Auth, caching, and cost controls
Use the same OAuth2/JWT across both. For REST, cache GETs by URL and ETag. For GraphQL, enable persisted queries, response caching by key, and complexity limits.

- Field-level permissions: In GraphQL, attach auth directives to types like Invoice.total or Customer.ssn.
- Rate fairness: Cap GraphQL by weighted cost, REST by requests per route.
- N+1 kills: Add dataloader resolvers and batch to the DB; watch p95.
Patterns on the generator
Our templates expose REST for commands and GraphQL for reads. The codegen produces /invoices, /payments, plus a finance graph with Invoice, PaymentIntent, and DunningPolicy.
- Mobile customer portal: GraphQL powers a "My Billing" screen with edges: customer → invoices → lastPayment.
- Accounting export: REST GET /invoices?modified_since=... streams NDJSON to your data lake reliably.
Migration playbook
- Start REST for external contracts. Layer a read-only GraphQL gateway.
- Introduce persisted GraphQL queries to freeze payloads for critical screens.
- Move list endpoints to GraphQL when clients need joins or cursor pagination.
- Keep payment mutations in REST for auditable idempotency keys.
Decision checklist
- Do consumers need joins and selective fields? GraphQL.
- Do auditors demand versioned, verb-aligned calls? REST.
- Is latency dominated by chatty screens? GraphQL with persisted queries.
- Are partner SLAs per endpoint? REST with clear quotas.
Observability and testing
Instrument both styles with trace IDs, input/output sampling, and synthetic checks. For GraphQL, log operation names and costs; for REST, log route and status buckets. Our generator scaffolds Playwright tests and contract tests so teams ship safely faster.




