GraphQL API
A GraphQL API generated from your database schema, in the dialect Hasura speaks. A client generated against Hasura — its queries, its codegen output, its endpoint — points at this server unchanged.
Endpoints
POST /v1/graphqlExecute a query or mutation. This is where a Hasura client sends them, and for most generated clients the only address they can be told about.
GET /v1/graphqlGraphQL Playground (interactive IDE)
GET /v1/graphql/wsSubscriptions over WebSocket, using the graphql-transport-ws protocol
POST /v1alpha1/graphqlThe address Hasura served before /v1, for a client old enough that it cannot be repointed
POST /api/graphqlThe same surface, for anything already pointed here
Shape of the schema
For a table author, with a to-many relationship articles:
| Root field | What it answers |
|---|---|
author(where:, order_by:, distinct_on:, limit:, offset:) | the rows |
author_by_pk(id: 1) | one row, or null |
author_aggregate(where: …) | aggregate { count sum { … } } and nodes { … } |
insert_author(objects:, on_conflict:) | affected_rows and returning { … } |
insert_author_one(object:, on_conflict:) | the row written |
update_author(where:, _set:, _inc:, …) | affected_rows and returning { … } |
update_author_by_pk(pk_columns: {id: 1}, _set:) | the row written |
update_author_many(updates: [{where, _set, …}]) | one mutation response per update |
delete_author(where:) | affected_rows and returning { … } |
delete_author_by_pk(id: 1) | the row removed |
The root types are named query_root, mutation_root and subscription_root. The subscription root mirrors the query root, and each of its fields is a live query.
Queries
query {
author(
where: { name: { _ilike: "%rust%" } }
order_by: [{ created_at: desc_nulls_last }, { name: asc }]
limit: 10
offset: 20
) {
id
name
articles(where: { published: { _eq: true } }, limit: 5) {
id
title
}
articles_aggregate {
aggregate {
count
}
}
}
}order_by takes a list of single-key objects, because ordering is ordered: {name: asc, id: desc} is one object whose two keys have no defined precedence, and the client that wrote it meant name first.
Filtering
where takes a generated <table>_bool_exp. Every comparison is named for the type it applies to, so an unknown operator or an ill-typed operand is refused by validation rather than by the database.
Any column
_eq _neq _gt _gte _lt _lte _in _nin _is_nullText
_like _nlike _ilike _nilike _similar _nsimilar _regex _iregex _nregex _niregexjson / jsonb
_contains _contained_in _has_key _has_keys_any _has_keys_all _jsonb_path_exists _jsonb_path_match _castltree
_ancestor _descendant _matches _matches_fulltext, and their _any formsPostGIS
_st_contains _st_crosses _st_equals _st_intersects _st_overlaps _st_touches _st_within _st_d_within _st_3d_d_within _castCombine them with _and, _or and _not, and follow a relationship by naming it. A question can also be asked about a whole related set rather than any one row of it:
{
authors(where: { books_aggregate: { count: { predicate: { _gt: 2 } } } }) {
name
}
}Over no related rows at all count is zero and the boolean folds are null — which is how “authors with no books” is written.
Mutations
mutation {
insert_author(
objects: [{ name: "Ada", articles: { data: [{ title: "On engines" }] } }]
on_conflict: { constraint: author_name_key, update_columns: [name] }
) {
affected_rows
returning { id name }
}
update_article_by_pk(pk_columns: { id: 7 }, _set: { published: true }) {
id
}
delete_article(where: { views: { _lt: 10 } }) {
affected_rows
}
}Nested writes, upserts with on_conflict, update_many and the document operators are all supported. A mutation naming several root fields runs them in one transaction: if any fails, none of them happened.
Subscriptions
Each subscription field is a live query — the answer now, and the answer again whenever it changes. Connect to ws://localhost:3000/v1/graphql/ws using the graphql-transport-ws protocol.
subscription {
article(where: { published: { _eq: true } }, order_by: [{ created_at: desc }], limit: 10) {
id
title
}
}The cursor-based half of Hasura’s subscription surface — _stream — is not implemented. See Realtime.
Authentication
A caller holding the admin secret is an administrator, and an administrator may ask to be treated as someone else. Other x-hasura-* headers become session variables that a row-level policy can read.
curl localhost:3000/v1/graphql \
-H 'X-Hasura-Admin-Secret: shh' \
-H 'X-Hasura-Role: user' \
-H 'X-Hasura-User-Id: 1' \
-d '{"query":"{ article { id title } }"}'With a token instead, the role comes from x-hasura-default-role, and an X-Hasura-Role header may select any role listed in the token’s x-hasura-allowed-roles. That list sits inside the signature, so a caller may choose among the identities it was issued and cannot add one.
With no admin secret configured, x-hasura-* headers are ignored entirely and session variables come only from a verified token. This is a deliberate divergence from Hasura, which treats every caller on an unsecured deployment as an administrator. See Configuration.
Errors
Errors come back in Hasura’s envelope, which client code branches on. There is no data key on failure, and the path names a place in the request rather than in the response.
{
"errors": [
{
"message": "field 'titel' not found in type: 'article'",
"extensions": {
"path": "$.selectionSet.article.selectionSet.titel",
"code": "validation-failed"
}
}
]
}Example request
curl -X POST http://localhost:3000/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ article(limit: 5) { id title author { name } } }"}'