Skip to content

From GraphQL

If you know GraphQL, you already understand half of MeshQL: clients pick fields, servers shape responses. The other half is different on purpose — fewer moving parts, SQL-native fetching, REST transport.

For the underlying mindset, read Thinking in MeshQL first. This page is the translation guide.

GraphQL MeshQL
Selection sets JSON selection or { brace syntax }
Nested objects Joins in schema + shaper
Typed client @meshql/client / meshql-client
Schema as contract MeshSchema entities + fields + joins
“Give me these fields” Same — via X-Mesh-Query or persisted query ID

Example — GraphQL:

query {
user(id: 1) {
id
name
tokens { accessToken }
}
}

MeshQL JSON selection (equivalent shape):

{ "user": { "id": true, "name": true, "tokens": { "accessToken": true } } }

QL syntax:

{ user { id name tokens { accessToken } } }

Transport: GET /mesh/user/1 with base64-encoded query in X-Mesh-Query. See HTTP wire spec.

GraphQL MeshQL Notes
type User entities.user Root queryable type
Object field joins["user.tokens"] Declared once, not per-request resolver
Field resolver (none) Fetch happens at entity level
Root / parent resolver mesh.resolve("user", fn) One function per entity
Query type HTTP routes + selection REST verbs, not a single POST endpoint
Fragment Shared JSON selection object or persisted query Reuse by ID in production
@deprecated Remove from fields allowlist Schema is an allowlist
Dataloader Not needed JoinPlan → one JOIN
DataLoader batching SQL JOIN or ORM nested select N+1 avoided by design
Mutation PUT / POST / DELETE on /mesh/... Same integrity/access stack
Subscription @meshql/sse + @meshql/pubsub SSE refresh, not GraphQL websocket protocol
Federation @meshql/gateway V1 (static multi-service stitch) Dynamic discovery later
Introspection No built-in Document schema in OpenAPI or TS types
SDL TypeScript schema config, ORM inference, or @meshql/codemods CLI converts SDL → schema + migration report
User.tokens → resolver runs → DB query per user (N+1)
→ dataloader patches over it
Client asks for user + tokens
→ JoinPlan includes user fields + tokens join
→ one resolver
→ one SQL query with JOIN
→ shaper nests JSON

You stop thinking “how do I resolve this field?” and start thinking “what does this entity read need to fetch?”

GraphQL

const resolvers = {
Query: {
user: (_, { id }) => db.user.findUnique({ where: { id } }),
},
User: {
tokens: (user) => db.token.findMany({ where: { userId: user.id } }),
},
};

MeshQL

mesh.resolve("user", async (plan) => {
const { sql, params } = buildSelectSql(plan, schema);
return db.query(sql, params); // one query, includes tokens if client asked
});

With Prisma:

withPrisma(mesh, prisma, { schema });
// plan → nested prisma.select from JoinPlan

GraphQL — often custom args + connection spec:

query {
users(first: 10, role: ADMIN) { id name }
}

MeshQL$list in selection (client SDK list option):

await client.query(
{ user: { id: true, name: true } },
{
list: {
limit: 10,
filter: [{ field: "role", op: "eq", value: "admin" }],
},
},
);

See List options spec.

GraphQL subscriptions are a persistent websocket with field-level publish/subscribe semantics.

MeshQL v0.9.0 takes a different, REST-native path:

  1. @meshql/pubsub — fan-out on entity/record channels (memory, Redis, Postgres).
  2. @meshql/sseGET /mesh/:entity/:id/events with the same selection headers as a point read.
  3. On mutation, call notifyEntityUpdate(pubsub, "post", id).
  4. Each subscriber gets an SSE update event with freshly shaped JSON.

Good fit when: browser clients, cache-friendly infra, “refresh this record when it changes.”

Not a drop-in for: GraphQL subscription filters, arbitrary event streams, or third-party GraphQL clients.

  • Introspection — no __schema query; use docs, TS types, or OpenAPI alongside MeshQL.
  • GraphQL ecosystem — Apollo, Relay, GraphiQL plugins don’t apply as-is.
  • Public third-party API — GraphQL’s self-documenting schema shines for external developers you don’t control.
  • Federation at basic level@meshql/gateway V1 ships static multi-service stitch; dynamic discovery is later
  • No dataloaders — joins are the answer.
  • HTTP caching — GET reads cache at CDN and browser.
  • Smaller stack — no codegen loop for many teams.
  • SQL/ORM-native — one query you can explain in EXPLAIN.
  • Incremental adoption — run beside /graphql.

MeshQL does not require a big bang:

app.use("/graphql", graphqlHandler);
app.use(meshExpressRouter(mesh, "/mesh"));
app.use(meshSseExpressRouter(mesh, { pubsub }, "/mesh"));

Suggested order:

  1. Week 1–2 — new features on /mesh only.
  2. Week 3–4 — migrate simple queries (single entity, no custom GraphQL middleware).
  3. Month 2 — migrate reads with N+1 pain (biggest win).
  4. Month 3+ — optional GraphQL retirement.

@meshql/codemods reads GraphQL SDL and emits MeshQL schema config, resolver stubs, and a migration report:

Terminal window
npx meshql-codemod graphql-sdl ./schema.graphql --out ./meshql-migration

Manual mapping for edge cases:

GraphQL type → MeshQL entity
GraphQL object field → MeshQL join
GraphQL resolver → mesh.resolve()
GraphQL dataloader → delete
GraphQL mutation → PUT/POST route + notifyEntityUpdate for SSE
GraphQL subscription → SSE + pubsub