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.

GraphQLMeshQL
Selection setsJSON selection or { brace syntax }
Nested objectsJoins in schema + shaper
Typed client@meshql/client / @meshqljs/client
Schema as contractMeshSchema 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": { "$select": { "id": true, "name": true, "tokens": { "$select": { "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.

GraphQLMeshQLNotes
type Userentities.userRoot queryable type
Object fieldjoins["user.tokens"]Declared once, not per-request resolver
Field resolver(none)Fetch happens at entity level
Root / parent resolvermesh.resolve("user", fn)One function per entity
Query typeHTTP routes + selectionREST verbs, not a single POST endpoint
FragmentShared JSON selection object or persisted queryReuse by ID in production
@deprecatedRemove from fields allowlistSchema is an allowlist
DataloaderNot neededJoinPlan → one JOIN
DataLoader batchingSQL JOIN or ORM nested selectN+1 avoided by design
MutationREST PUT/POST/DELETE on /mesh/{entity}/… (same URLs as reads)Not a GraphQL mutation DSL — Reads and writes
Subscription@meshql/sse + @meshql/pubsubSSE refresh, not GraphQL websocket protocol
Federation@meshql/gateway V1 (static multi-service stitch)Dynamic discovery later
IntrospectionNo built-inDocument schema in OpenAPI or TS types
SDLTypeScript schema config, ORM inference, or @meshql/codemodsCLI 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 — read controls live on the canonical query node:

await client.query(
{
user: {
$select: { id: true, name: true },
$page: { first: 10 },
$where: { field: "role", op: "eq", value: "admin" },
},
},
);

See the query controls reference.

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/sse — GET /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 → REST PUT/POST/DELETE under /mesh/{entity}/… (URLs, not $connect)
GraphQL subscription → SSE + pubsub

See Reads and writes.