rest.graphql.win

🎓 MCP + CodeMode 🧠 REST vs GraphQL

One lab, one dataset, one business question. We build the same SaaS support and billing domain twice - once as a REST API, once as a GraphQL API - then wrap both in an MCP server and let an agent drive them through a CodeMode sandbox. Every claim on this page is measured, not guessed.

FastAPI + SQLite (OLTP) Strawberry + DuckDB (OLAP) MCP over Streamable HTTP CodeMode in a Monty sandbox OpenTelemetry traces

The 2x2 matrix

Two API styles times two storage engines. The upper layer is the agent facing surface: the same operations published as MCP tools, plus a CodeMode sandbox where the model writes Python instead of juggling tool payloads.

MCP server + CodeMode sandbox tools/list · tools/call · exec(code) · host functions · per call spans exposure switch: MCP_EXPOSURE=all | rest | graphql | curated API style Storage engine SQLite (OLTP) DuckDB (OLAP) REST multi endpoint, resource shaped GET /api/accounts?status=past_due GET /api/tickets?account_id=42 PATCH /api/tickets/7 (If-Match) REST same style, analytical payloads GET /api/analytics/revenue-by-plan GET /api/analytics/tickets-at-risk POST /api/analytics/query GraphQL single endpoint, field shaped query { accounts(status: PAST_DUE) { id } } mutation { updateTicket(id: 7, version: 3) } errors[] with extensions.code, HTTP 200 GraphQL same style, analytical payloads analytics { revenueByPlan(from, to) } analytics { ticketsAtRisk { accountId } } analytics { runSql(sql: "select ...") } one shared service layer underneath, so every measured difference comes from the API style

Scroll the diagram sideways to see the whole matrix.

The same question, two ways

"How much revenue is at risk from accounts with an overdue invoice and open critical tickets?" REST needs a small chain of calls, GraphQL needs one. Below the panels, press the button: if the lab is running on 127.0.0.1:8000 you get the live answer, otherwise the recorded output from the seeded dataset.

REST 3 to 4 round trips

# 1. accounts that owe money
curl 'http://127.0.0.1:8000/api/accounts?status=past_due&limit=50'

# 2. their invoices
curl 'http://127.0.0.1:8000/api/invoices?status=overdue&limit=50'

# 3. their tickets, one account at a time
curl 'http://127.0.0.1:8000/api/tickets?account_id=42&status=open'

# 4. the plan behind the account
curl 'http://127.0.0.1:8000/api/accounts/42'

GraphQL 1 round trip

query RevenueAtRisk {
  accounts(status: PAST_DUE, limit: 50) {
    items {
      id
      name
      invoices(status: OVERDUE) { totalCents dueAt }
      tickets(status: OPEN, priority: CRITICAL) {
        id subject priority
      }
    }
  }
}

Try it against the running lab

not connected yet
Press "Run the query". The page calls http://127.0.0.1:8000 in your browser.
If the lab is not running you get the recorded output from the seeded dataset instead.

The three panels

The sketches that started the project. Each one becomes a chapter with runnable commands, numbers and traces.

Comparison card of REST versus GraphQL, multi endpoint versus single endpoint
REST vs GraphQL reading and updating data
  • Many endpoints versus one endpoint
  • Fixed payloads versus exactly the fields you ask for
  • Dedicated URLs versus one mutation with selected fields

The lab measures round trips, the N+1 trap, error shapes, caching and versioning.

Comparison card of OLTP versus OLAP, fast and frequent versus deep and complex
OLTP vs OLAP running the business versus explaining it
  • Live rows versus consolidated columns
  • Write heavy versus read heavy
  • SQLite and DuckDB, side by side on the same question

SQLite holds the operational truth, an ETL materialises the star schema into DuckDB.

Comparison card of REST and GraphQL versus MCP and CodeMode, manual assembly versus agentic and automated
REST / GraphQL vs MCP + CodeMode manual assembly versus agent driven
  • One tool call per row versus one program
  • Every payload enters the context versus none of them
  • The same catalog of operations underneath

The model pays for the tool schemas once, the intermediate payloads never enter the context.

Run the lab yourself

One pixi environment, one ASGI app, one port. The chapters are markdown files in the repo, every command is copy and paste ready.

1. Seed and build the star schema

pixi run seed # small preset, deterministic pixi run etl # SQLite to DuckDB pixi run seed -- --scale large

2. Serve REST, GraphQL and MCP

pixi run serve # http://127.0.0.1:8000 pixi run serve -- --tls # https://127.0.0.1:8443, mkcert # GraphiQL lives at /graphql

3. Measure the differences

pixi run bench # calls, round trips, ms, bytes pixi run tools # MCP catalog and token cost pixi run savings # native MCP versus CodeMode

The chapters

The stack

  • FastAPI for the REST side and the OpenAPI document
  • Strawberry for the GraphQL schema and GraphiQL
  • SQLite as the operational database, WAL mode
  • DuckDB as the analytical database, columnar
  • MCP Python SDK serving Streamable HTTP at /mcp
  • Pydantic Monty as the CodeMode sandbox
  • tiktoken to count what the model actually pays
  • OpenTelemetry with a JSONL exporter and OTLP
  • pixi holding the whole environment
  • pytest and a verify script for the whole stack