GraphQL – Ponder
Skip to content

GraphQL

Query the autogenerated GraphQL API

Ponder automatically generates a GraphQL API based on the tables defined in ponder.schema.ts.

The GraphQL API is the quickest and easiest way to query data from a Ponder app.

Register graphql middleware

To enable the GraphQL API, register the graphql Hono middleware.

src/api/index.ts
import { db } from "ponder:api";
import schema from "ponder:schema";
import { Hono } from "hono";
import { graphql } from "ponder"; 
 
const app = new Hono();
 
app.use("/graphql", graphql({ db, schema })); 
 
export default app;

The graphql() middleware includes a GraphiQL interface in response to GET requests (you can visit it in your browser). GraphiQL is a useful tool to explore your schema and test queries.

Schema generation

The GraphQL schema includes a singular and a plural query field for each table in ponder.schema.ts.

If your schema contains a person table, the GraphQL schema will include a person and a persons field on the root Query type. The singular query field returns a single record (or null) and the plural query field returns a page of records.

ponder.schema.ts
import { onchainTable } from "ponder";
 
export const person = onchainTable("person", (t) => ({
  id: t.integer().primaryKey(),
  name: t.text().notNull(),
  age: t.integer(),
}));

Filtering

Use the where argument to filter for records that match certain criteria. The where argument type includes filter options for every column defined on a table. Here are the filter options available for each column type.

Filter optionAvailable for column typesInclude records where {column}...
{column}Allequals the value
{column}_notAlldoes not equal the value
{column}_inAll primitives and enumsis one of the values
{column}_not_inAll primitives and enumsis not one of the values
{column}_gtNumeric primitivesis greater than the value
{column}_ltNumeric primitivesis less than the value
{column}_gteNumeric primitivesis greater than or equal to the value
{column}_lteNumeric primitivesis less than or equal to the value
{column}_containsString primitivescontains the substring
{column}_not_containsString primitivesdoes not contain the substring
{column}_starts_withString primitivesstarts with the substring
{column}_not_starts_withString primitivesdoes not start with the substring
{column}_ends_withString primitivesends with the substring
{column}_not_ends_withString primitivesdoes not end with the substring
{column}_hasLists of primitives and enumshas the value as an element
{column}_not_hasLists of primitives and enumsdoes not have the value as an element

You can compose filters using the AND and OR operators. These special fields accept an array of filter objects.

Examples

For the following examples, assume these records exist in your database.

person data
[
  { "id": 1, "name": "Barry", "age": 57 },
  { "id": 2, "name": "Lucile", "age": 32 },
  { "id": 3, "name": "Sally", "age": 22 },
  { "id": 4, "name": "Pablo", "age": 71 },
]

Get all person records with an age greater than 32.

Query
query {
  persons(where: { age_gt: 32 }) {
    name
    age
  }
}

Get all person records with a name that does not end with "y" and an age greater than 60. Note that when you include multiple filter conditions, they are combined with a logical AND.

Query
query {
  persons(
    where: {
      AND: [
        { name_not_ends_with: "y" },
        { age_gte: 60 }
      ]
    }
  ) {
    name
    age
  }
}

Get all person records with a name that contains "ll" or an age greater than or equal to 50. In this case, we use the special OR operator to combine multiple filter conditions.

Query
query {
  persons(
    where: {
      OR: [
        { name_contains: "ll" },
        { age_gte: 50 }
      ] 
    }
  ) {
    name
    age
  }
}

Sorting

Use the orderBy and orderDirection arguments to sort the result by a column. By default, the result is sorted by the primary key column(s) in ascending order.

Pagination optionDefault
orderByPrimary key column(s)
orderDirection"asc"

Examples

Query
query {
  persons(orderBy: "age", orderDirection: "desc") {
    name
    age
  }
}

Pagination

The GraphQL API supports cursor pagination with an API that's inspired by the Relay GraphQL Cursor Connection specification.

Plural fields and p.many() relationship fields each return a Page type. This object contains a list of items, a PageInfo object, and the total count of records that match the query.

ponder.schema.ts
import { onchainTable } from "ponder";
 
export const pet = onchainTable("pet", (t) => ({
  id: t.text().primaryKey(),
  name: t.text().notNull(),
}));

Page info

The PageInfo object contains information about the position of the current page within the result set.

nametype
startCursorStringCursor of the first record in items
endCursorStringCursor of the last record in items
hasPreviousPageBoolean!Whether there are more records before this page
hasNextPageBoolean!Whether there are more records after this page

Total count

The totalCount field returns the number of records present in the database that match the specified query. The value is the same regardless of the current pagination position and the limit argument. Only the where argument can change the value of totalCount.

Cursor values

Each cursor value is an opaque string that encodes the position of a record in the result set.

  • Cursor values should not be decoded or manipulated by the client. The only valid use of a cursor value is an argument, e.g. after: previousPage.endCursor.
  • Cursor pagination works with any filter and sort criteria. However, do not change the filter or sort criteria between paginated requests. This will cause validation errors or incorrect results.

Examples

As a reminder, assume that these records exist in your database for the following examples.

person data
[
  { "id": 1, "name": "Barry", "age": 57 },
  { "id": 2, "name": "Lucile", "age": 32 },
  { "id": 3, "name": "Sally", "age": 22 },
  { "id": 4, "name": "Pablo", "age": 71 },
]

First, make a request without specifying any pagination options. The items list will contain the first n=limit records that match the filter and sort criteria.

Query 1
query {
  persons(orderBy: "age", orderDirection: "asc", limit: 2) {
    items {
      name
      age
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
    totalCount
  }
}

To paginate forwards, pass pageInfo.endCursor from the previous request as the after option in the next request.

Query 2
query {
  persons(
    orderBy: "age",
    orderDirection: "asc",
    limit: 2,
    after: "Mxhc3NDb3JlLTA="
  ) {
    items {
      name
      age
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
    totalCount
  }
}

To paginate backwards, pass pageInfo.startCursor from the previous request as the before option in the next request.

Query 3
query {
  persons(
    orderBy: "age",
    orderDirection: "asc",
    limit: 2,
    before: "MxhcdoP9CVBhY"
  ) {
    items {
      name
      age
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
    totalCount
  }
}

Relationship fields

The GraphQL schema includes a relationship field for each one or many relation defined in your schema. One-to-many fields are very similar to the top-level plural query field, except they are automatically filtered by the parent row ID.

ponder.schema.ts
import { onchainTable, relations } from "ponder";
 
export const pet = onchainTable("pet", (t) => ({
  id: t.text().primaryKey(),
  name: t.text().notNull(),
  ownerId: t.integer().notNull(),
}));
 
export const petRelations = relations(pet, ({ one }) => ({
  owner: one(person, { fields: [pet.ownerId], references: [person.id] }),
}));
 
export const person = onchainTable("person", (t) => ({
  id: t.integer().primaryKey(),
}));
 
export const personRelations = relations(person, ({ many }) => ({
  dogs: many(pet),
}));

Performance tips

Here are a few tips for speeding up slow GraphQL queries.

  1. Limit query depth: Each layer of depth in a GraphQL query introduces at least one additional sequential database query. Avoid queries that are more than 2 layers deep.
  2. Use pagination: Use cursor-based pagination to fetch records in smaller, more manageable chunks. This can help reduce the load on the database.
  3. Consider database indexes: Consider creating database indexes to speed up filters, joins, and sort conditions. Keep in mind that relations do not automatically create indexes. Read more.
  4. Enable horizontal scaling: If the GraphQL API is struggling to keep up with request volume, consider spreading the load across multiple instances. Read more.