Skip to content

Quickstart

Get a minimal MeshQL server running in a few minutes — then test with curl.

For the full step-by-step guide (Express, Hono, Bun, curl examples), see Run example.

MeshQL is published on JSR (@meshql/*) and npm (meshql-*).

npm:

Terminal window
npm install meshql-core meshql-http meshql-client
npm i express
npm i -D typescript tsx @types/node @types/express

JSR:

Terminal window
npx jsr add @meshql/core @meshql/http @meshql/client
npm i express
npm i -D typescript tsx @types/node @types/express

Bun: bun add meshql-core meshql-http meshql-client express or bunx jsr add @meshql/core @meshql/http @meshql/client && bun add express

Deno: deno add jsr:@meshql/core jsr:@meshql/http jsr:@meshql/client

npm imports use package names (meshql-core, meshql-http, meshql-client). JSR imports use @meshql/*. Examples below use JSR paths; swap names if you installed from npm.

import { createMesh, type MeshSchema } from "@meshql/core";
import { meshExpressRouter } from "@meshql/http/express";
import express from "express";
const schema: MeshSchema = {
entities: {
user: { fields: ["id", "name"], table: "users" },
token: {
fields: ["accessToken"],
table: "tokens",
columns: { accessToken: "access_token" },
},
},
joins: {
"user.tokens": {
entity: "token",
on: "tokens.user_id = users.id",
type: "many",
},
},
};
const mesh = createMesh(schema);
mesh.resolve("user", async () => [
{ user_id: 1, user_name: "Ada", tokens_accessToken: "tok_abc" },
]);
const app = express();
app.use(express.json());
app.use(meshExpressRouter(mesh, "/mesh"));
app.listen(3001, () => console.log("http://localhost:3001/mesh"));

Join keys are always {entityKey}.{ref} (here user.tokens). Nested relations, many-to-many, and polymorphic associations are covered in Relations.

Start: npx tsx index.ts

Terminal window
Q=$(echo -n '{"user":{"$select":{"id":true,"name":true,"tokens":{"$select":{"accessToken":true}}}}}' | base64 | tr -d '\n')
curl -s "http://localhost:3001/mesh/user/1" \
-H "X-Mesh-Query: $Q" \
-H "X-Mesh-Format: json"
import { createClient } from "@meshql/client";
const client = createClient({ url: "http://localhost:3001/mesh" });
const user = await client.query(
{
user: {
$select: {
id: true,
name: true,
tokens: { $select: { accessToken: true } },
},
},
},
{ entityId: "1" },
);
console.log(user);

For production apps, register queries once and send a ~10-byte X-Mesh-Query-Id header instead of a ~300-byte X-Mesh-Query payload.

Server — swap meshExpressRouter for the persisted-queries router:

import { meshPersistedQueriesExpressRouter } from "@meshql/persisted-queries/express";
app.use(meshPersistedQueriesExpressRouter(mesh, {}, "/mesh"));

Register (once per query shape):

Terminal window
curl -s -X POST http://localhost:3001/mesh/queries \
-H "Content-Type: application/json" \
-d '{"query":"{\"user\":{\"$select\":{\"id\":true,\"name\":true,\"tokens\":{\"$select\":{\"accessToken\":true}}}}}","format":"json"}'
# → { "id": "q_a3f1b2c8" }

Execute with the ID:

Terminal window
curl -s "http://localhost:3001/mesh/user/1" \
-H "X-Mesh-Query-Id: q_a3f1b2c8" \
-H "X-Mesh-Format: json"

Client — transparent mode (auto-registers on first use):

const client = createClient({
url: "http://localhost:3001/mesh",
persistedQueries: true,
});
const user = await client.query(
{ user: { $select: { id: true, name: true, tokens: { $select: { accessToken: true } } } } },
{ entityId: "1" },
);

Details: persisted-queries package · HTTP wire spec

The full express-postgres example supports in-memory and Postgres modes.