Decorators
Decorators — Define schema elements
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}| Scalar | GraphQL | JSON Format |
|---|---|---|
ID | ID | UUID string |
String | String | UTF-8 string |
Int | Int | 32-bit integer |
Float | Float | Double precision |
Boolean | Boolean | true/false |
DateTime | DateTime | ISO 8601 |
Date | Date | YYYY-MM-DD |
Time | Time | HH:MM:SS |
Decimal | Decimal | String (precision) |
JSON | JSON | Any 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/playgroundFeatures:
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 /graphqlContent-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"}| Header | Description |
|---|---|
Authorization | Bearer token for auth |
X-Request-ID | Request tracing ID |
Content-Type | application/json |
Decorators
Decorators — Define schema elements
Operators
Operators — Filter operators
Scalars
Scalars — Type reference
Pagination
Pagination — Cursor and offset pagination, connection types
Custom Queries
Custom Queries — sql_source mapping, full-text search, sorting