Skip to content

GraphQL API

This page documents the GraphQL transport. FraiseQL also serves REST and gRPC from the same compiled schema. See Transport Overview, REST Transport, and REST API Reference.

FraiseQL generates a complete GraphQL API from your schema. This reference covers all generated operations.

type Query {
# Entity queries (declared with @fraiseql.query)
user(id: ID!): User
users(where: UserWhereInput, limit: Int, offset: Int, orderBy: UserOrderByInput): [User!]!
# Relationship queries
postsByAuthor(authorId: ID!, limit: Int): [Post!]!
}
type Mutation {
# Explicitly declared mutations (backed by PostgreSQL functions)
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
# Custom mutations
publishPost(id: ID!): Post!
}
type Subscription {
# Real-time updates (declared with @fraiseql.subscription)
userCreated: User!
postUpdated(authorId: ID): Post!
}
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
createdAt
}
}

Variables:

{
"id": "550e8400-e29b-41d4-a716-446655440000"
}

Response:

{
"data": {
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
}
}
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
id
name
email
}
}

With Filtering:

query GetActiveUsers {
users(where: { isActive: { _eq: true } }, limit: 20) {
id
name
}
}

With Ordering:

query GetRecentUsers {
users(orderBy: { createdAt: DESC }, limit: 10) {
id
name
createdAt
}
}
query GetPostWithAuthor($id: ID!) {
post(id: $id) {
id
title
content
author {
id
name
email
}
comments {
id
content
author {
name
}
}
}
}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
name
createdAt
}
}

Variables:

{
"input": {
"email": "new@example.com",
"name": "New User",
"bio": "Hello world"
}
}
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
bio
updatedAt
}
}

Variables:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"input": {
"name": "Updated Name",
"bio": "New bio"
}
}
mutation DeleteUser($id: ID!) {
deleteUser(id: $id)
}

Response:

{
"data": {
"deleteUser": true
}
}
mutation PublishPost($id: ID!) {
publishPost(id: $id) {
id
isPublished
publishedAt
}
}

When a mutation has cascade enabled, the response includes all affected entities for automatic client-side cache updates:

mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
cascade {
updated {
id
operation
entity
}
deleted {
id
operation
entity
}
invalidations {
queryName
strategy
scope
}
metadata {
timestamp
affectedCount
depth
transactionId
}
}
}
}

Response:

{
"data": {
"createPost": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"title": "Hello World",
"cascade": {
"updated": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"operation": "CREATED",
"entity": { "id": "550e8400-...", "title": "Hello World" }
},
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"operation": "UPDATED",
"entity": { "id": "6ba7b810-...", "name": "Alice", "postCount": 5 }
}
],
"deleted": [],
"invalidations": [
{ "queryName": "posts", "strategy": "INVALIDATE", "scope": "all" }
],
"metadata": {
"timestamp": "2025-01-15T10:30:00Z",
"affectedCount": 2,
"depth": 1,
"transactionId": null
}
}
}
}
}

When any mutation enables cascade, the compiler adds these types to the schema:

type Cascade {
updated: [CascadeEntity!]!
deleted: [CascadeEntity!]!
invalidations: [CascadeInvalidation!]!
metadata: CascadeMetadata!
}
type CascadeEntity {
id: String!
operation: String!
entity: JSON!
}
type CascadeInvalidation {
queryName: String!
strategy: String!
scope: String!
}
type CascadeMetadata {
timestamp: String!
affectedCount: Int!
depth: Int!
transactionId: String
}

These types are generated once and shared across all cascade-enabled mutations. The cascade field is only present on mutation success types that have cascade enabled — either globally via [cascade] enabled = true in TOML or per-mutation via cascade=True on the decorator.

subscription OnUserCreated {
userCreated {
id
name
email
}
}
subscription OnPostUpdated($authorId: ID) {
postUpdated(authorId: $authorId) {
id
title
updatedAt
}
}

The server implements both graphql-transport-ws (modern) and graphql-ws (Apollo legacy) protocols, negotiated via the Sec-WebSocket-Protocol header. The default WebSocket path is /ws.

The graphql-transport-ws protocol requires a connection_init handshake before subscribing:

const ws = new WebSocket('ws://localhost:8080/ws', 'graphql-transport-ws');
ws.onopen = () => {
// Step 1: Initialize connection
ws.send(JSON.stringify({ type: 'connection_init', payload: {} }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'connection_ack') {
// Step 2: Send subscription after ack
ws.send(JSON.stringify({
type: 'subscribe',
id: '1',
payload: {
query: `subscription { userCreated { id name } }`
}
}));
} else if (msg.type === 'next') {
console.log('New user:', msg.payload.data.userCreated);
}
};
input CreateUserInput {
email: String!
name: String!
bio: String
avatarUrl: String
}
input UpdateUserInput {
name: String
bio: String
avatarUrl: String
}

All fields optional - only provided fields are updated.

input UserWhereInput {
id: IDFilter
email: StringFilter
name: StringFilter
isActive: BooleanFilter
createdAt: DateTimeFilter
_and: [UserWhereInput!]
_or: [UserWhereInput!]
_not: UserWhereInput
}
input StringFilter {
_eq: String
_neq: String
_in: [String!]
_nin: [String!]
_like: String
_ilike: String
_is_null: Boolean
}
input UserOrderByInput {
id: OrderDirection
name: OrderDirection
email: OrderDirection
createdAt: OrderDirection
}
enum OrderDirection {
ASC
DESC
ASC_NULLS_FIRST
ASC_NULLS_LAST
DESC_NULLS_FIRST
DESC_NULLS_LAST
}
ScalarGraphQLJSON Format
IDIDUUID string
StringStringUTF-8 string
IntInt32-bit integer
FloatFloatDouble precision
BooleanBooleantrue/false
DateTimeDateTimeISO 8601
DateDateYYYY-MM-DD
TimeTimeHH:MM:SS
DecimalDecimalString (precision)
JSONJSONAny JSON value
{
"errors": [{
"message": "Variable '$email' expected type 'String!' but got null",
"locations": [{"line": 1, "column": 15}],
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED"
}
}]
}
{
"data": {
"user": null
}
}
{
"errors": [{
"message": "Forbidden: missing scope read:User.email",
"path": ["user", "email"],
"extensions": {
"code": "FORBIDDEN"
}
}]
}
{
"errors": [{
"message": "User with email already exists",
"path": ["createUser"],
"extensions": {
"code": "DUPLICATE_EMAIL"
}
}]
}

Introspection is disabled by default. Enable it with fraiseql run --introspection.

query {
__schema {
types {
name
kind
}
}
}
query {
__type(name: "User") {
name
fields {
name
type {
name
kind
}
}
}
}

Access the GraphQL playground at:

http://localhost:8080/playground

Features:

  • Interactive query editor
  • Schema explorer
  • Variable editor
  • Response viewer
  • Query history

Send multiple operations in one request:

[
{
"query": "query { user(id: \"1\") { name } }"
},
{
"query": "query { user(id: \"2\") { name } }"
}
]

Response:

[
{"data": {"user": {"name": "User 1"}}},
{"data": {"user": {"name": "User 2"}}}
]

Limit: Batch size limits are enforced at the server level. The [graphql] TOML namespace is not a verified fraiseql.toml section — check the TOML Config Reference for current batch configuration options.

POST /graphql
Content-Type: application/json
{
"query": "query GetUser($id: ID!) { user(id: $id) { name } }",
"operationName": "GetUser",
"variables": {
"id": "123"
}
}
GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"..."}}
&variables={"id":"123"}
HeaderDescription
AuthorizationBearer token for auth
X-Request-IDRequest tracing ID
Content-Typeapplication/json

Decorators

Decorators — Define schema elements

Scalars

Scalars — Type reference

Pagination

Pagination — Cursor and offset pagination, connection types

Custom Queries

Custom Queries — sql_source mapping, full-text search, sorting