Skip to main content

GraphQL API

The GraphQL API is the primary interface for querying and mutating data in hugr. It provides a standardized GraphQL endpoint that accepts queries in the standard GraphQL format over HTTP.

Overview

The GraphQL API endpoint provides:

  • Standard GraphQL Protocol: Full compliance with the GraphQL specification
  • Flexible Queries: Read operations with filtering, sorting, pagination, and relationships
  • Mutations: Create, update, and delete operations with transaction support
  • Introspection: Full schema introspection for tools and clients
  • Authentication: Integrated with hugr's authentication system (API keys, JWT, OIDC, anonymous)
  • Access Control: Role-based permissions applied automatically

Endpoint Details

Path: /query

Methods: GET, POST

Content-Type: application/json

Request Format

POST Requests (Recommended)

POST requests are the standard way to send GraphQL queries. They support all GraphQL features including variables and operation names.

Headers

Content-Type: application/json

Optional authentication:

Authorization: Bearer <token>

Request Body

{
"query": "<graphql_query>",
"variables": {
"var1": "value1",
"var2": "value2"
},
"operationName": "<operation_name>"
}

Fields:

  • query (string, required): GraphQL query or mutation text
  • variables (object, optional): Variables used in the query
  • operationName (string, optional): Name of the operation to execute (for documents with multiple operations)

Example

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token-here" \
-d '{
"query": "query GetUsers($limit: Int!) { users(limit: $limit) { id name email } }",
"variables": {
"limit": 10
},
"operationName": "GetUsers"
}'

GET Requests

GET requests support simple queries without variables. They are useful for bookmarking, caching, and simple integrations.

Query Parameters

  • query (required): URL-encoded GraphQL query
  • variables (optional): URL-encoded JSON object with variables
  • operationName (optional): Operation name

Example

curl -X GET "http://localhost:8080/query?query=%7B%20users%20%7B%20id%20name%20email%20%7D%20%7D"

With variables:

curl -X GET "http://localhost:8080/query?query=query%20GetUser(%24id%3A%20Int!)%20%7B%20users(filter%3A%20%7Bid%3A%20%7Beq%3A%20%24id%7D%7D)%20%7B%20id%20name%20%7D%20%7D&variables=%7B%22id%22%3A%201%7D"

Note: GET requests have URL length limitations. Use POST for complex queries.

Response Format

Success Response

HTTP Status: 200 OK

Body:

{
"data": {
"users": [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
]
},
"extensions": {}
}

The response follows the standard GraphQL response format:

  • data: Query results (null if errors occurred)
  • extensions: Additional metadata (e.g., JQ transformation results, execution time)
  • errors (optional): Array of errors if any occurred

Partial Error Response

GraphQL can return partial data even when some fields have errors:

HTTP Status: 200 OK

Body:

{
"data": {
"users": [
{"id": 1, "name": "Alice", "email": null}
]
},
"errors": [
{
"message": "Field 'email' access denied",
"locations": [{"line": 1, "column": 20}],
"path": ["users", 0, "email"]
}
]
}

Error Response

When the query cannot be executed at all:

HTTP Status: 200 OK (GraphQL convention), 400 Bad Request, or 401 Unauthorized

Body:

{
"data": null,
"errors": [
{
"message": "Cannot query field 'invalid_field' on type 'User'",
"locations": [{"line": 1, "column": 15}]
}
]
}

HTTP Status Codes

CodeDescription
200 OKRequest processed (may contain GraphQL errors)
400 Bad RequestMalformed GraphQL query or invalid JSON
401 UnauthorizedMissing or invalid authentication
403 ForbiddenAccess denied by authorization rules
500 Internal Server ErrorServer-side error during query execution

Authentication

The GraphQL API integrates with hugr's authentication system. All authentication methods are supported:

API Key Authentication

Include the API key in the Authorization header:

curl -X POST http://localhost:8080/query \
-H "Authorization: Bearer service_key_abc123" \
-H "X-API-Username: api_service" \
-H "X-API-User-ID: svc_001" \
-d '{"query": "{ users { id name } }"}'

See Authentication Setup for API key configuration.

JWT/OIDC Authentication

Include the JWT token:

curl -X POST http://localhost:8080/query \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-d '{"query": "{ users { id name } }"}'

For web applications, tokens can be passed via cookies:

curl -X POST http://localhost:8080/query \
-H "Cookie: hugr_session=eyJhbGciOiJIUzI1NiIs..." \
-d '{"query": "{ users { id name } }"}'

The cookie name is configured with the OIDC_COOKIE_NAME environment variable (default: hugr_session).

Anonymous Access

If anonymous access is enabled, requests without authentication are assigned the anonymous role:

curl -X POST http://localhost:8080/query \
-d '{"query": "{ public_data { id name } }"}'

Anonymous users only see data permitted for their role based on permissions configured in the role_permissions table. Fields with hidden: true are not visible in introspection but can be explicitly requested. Fields with disabled: true are completely inaccessible.

GraphQL Introspection

The GraphQL API supports full introspection, allowing tools and clients to discover the schema dynamically.

Full Schema Introspection

query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
name
kind
description
fields {
name
description
type {
name
kind
}
}
}
}
}

Type Introspection

Query specific types:

query TypeIntrospection {
__type(name: "User") {
name
kind
description
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}

Role-Based Schema Visibility

Introspection results respect access control rules defined in the role_permissions table:

  • Fields with hidden: false (default): Visible in schema and queries
  • Fields with hidden: true: Not shown in introspection, but can be explicitly requested
  • Fields with disabled: true: Completely inaccessible and not visible in introspection

Each role sees only the types and fields permitted by their permissions. If a type/field has no permission entry for a role, it is accessible by default (unless restricted by a wildcard permission).

This ensures that the schema exposed to clients matches their actual access permissions.

Examples

Simple Query

Fetch users:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "{ users { id name email } }"
}'

Query with Filtering

Filter by condition:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "{ users(filter: { status: { eq: \"active\" } }) { id name email } }"
}'

Query with Variables

Use variables for dynamic queries:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "query GetUser($userId: Int!) { users(filter: { id: { eq: $userId } }) { id name email } }",
"variables": {
"userId": 123
}
}'

Query with Relationships

Fetch related data:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "{ users { id name orders { id total created_at } } }"
}'

Mutation

Create a new record:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{
"query": "mutation CreateUser($data: UsersInput!) { users { insert(data: $data) { id name email } } }",
"variables": {
"data": {
"name": "John Doe",
"email": "john@example.com",
"status": "active"
}
}
}'

Batch Mutation

Insert multiple records:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{
"query": "mutation CreateUsers($users: [UsersInput!]!) { users { insert_batch(data: $users) { id name } } }",
"variables": {
"users": [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"}
]
}
}'

Aggregation Query

Calculate statistics:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "{ users_aggregation { _rows_count created_at { min max } } }"
}'

Spatial Query

Query geospatial data:

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "{ locations(filter: { geometry: { st_within: { type: \"Polygon\", coordinates: [[[0,0],[10,0],[10,10],[0,10],[0,0]]] } } }) { id name geometry } }"
}'

CORS Configuration

For web applications making requests from browsers, configure CORS:

CORS_ALLOWED_ORIGINS=http://localhost:3000,https://app.example.com
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization

See Configuration for details.

Performance Considerations

1. Query Complexity

Control query depth to prevent excessive nesting:

MAX_DEPTH=7  # Default maximum query depth

2. Parallel Execution

Enable parallel query execution for better performance:

ALLOW_PARALLEL=true
MAX_PARALLEL_QUERIES=10 # 0 for unlimited

3. Connection Pooling

Configure database connection pools:

DB_MAX_OPEN_CONNS=10
DB_MAX_IDLE_CONNS=5
DB_PG_CONNECTION_LIMIT=64 # For PostgreSQL sources

4. Caching

Use GraphQL directives or HTTP caching:

query GetStaticData {
reference_data @cache(ttl: 3600) {
id
name
}
}

See Cache Directives for more details.

Best Practices

1. Use POST for Complex Queries

GET requests have URL length limitations. Always use POST for:

  • Queries with variables
  • Mutations
  • Complex nested queries
  • Queries with large filter conditions

2. Leverage Variables

Use variables instead of string interpolation:

# Good: Use variables
query GetUser($id: Int!) {
users(filter: { id: { eq: $id } }) {
id name
}
}

# Avoid: String interpolation (security risk)
query {
users(filter: { id: { eq: 123 } }) {
id name
}
}

3. Request Only Needed Fields

Avoid over-fetching:

# Good: Specific fields
{ users { id name } }

# Avoid: Fetching unnecessary data
{ users { id name email phone address city country created_at updated_at } }

4. Use Filtering at Database Level

Apply filters in GraphQL, not in application code:

# Good: Filter in GraphQL
{ users(filter: { status: { eq: "active" } }) { id name } }

# Avoid: Fetch all and filter in code
{ users { id name status } }

5. Handle Errors Gracefully

Always check for errors in the response:

const response = await fetch('/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '...' })
});

const result = await response.json();

if (result.errors) {
console.error('GraphQL errors:', result.errors);
// Handle errors
}

if (result.data) {
// Use data
}

6. Use Operation Names

Name your queries for better debugging:

query GetUserOrders($userId: Int!) {
users(filter: { id: { eq: $userId } }) {
id
name
orders {
id
total
}
}
}

7. Enable Persistent Queries

For production, consider using persisted queries to:

  • Reduce payload size
  • Improve security (only allow pre-approved queries)
  • Enable better caching

Security Considerations

1. Always Use HTTPS in Production

Encrypt all traffic:

server {
listen 443 ssl;
server_name api.example.com;

location /query {
proxy_pass http://hugr:8080;
}
}

2. Implement Rate Limiting

Prevent abuse with rate limiting:

limit_req_zone $binary_remote_addr zone=graphql:10m rate=10r/s;

location /query {
limit_req zone=graphql burst=20;
proxy_pass http://hugr:8080;
}

3. Validate Input

Use GraphQL variables and types to validate input:

query GetUser($id: Int!) {  # Type validation
users(filter: { id: { eq: $id } }) {
id
name
}
}

4. Configure Access Control

Use role-based permissions to restrict access. Permissions are managed through the role_permissions table:

mutation {
core {
# Hide email field for viewer role
insert_role_permissions(data: {
role: "viewer"
type_name: "users"
field_name: "email"
hidden: true
}) {
role
type_name
field_name
}

# Disable password field for all non-admin roles
insert_role_permissions(data: {
role: "viewer"
type_name: "users"
field_name: "password"
disabled: true
}) {
role
type_name
field_name
}
}
}

See Access Control for details.

5. Monitor Query Complexity

Enable query depth limits:

MAX_DEPTH=7

6. Sanitize Sensitive Data

Remove sensitive fields from responses using permissions or transformations.

Troubleshooting

Connection Refused

Error: Connection refused or ECONNREFUSED

Solutions:

  1. Check hugr is running: curl http://localhost:8080/query
  2. Verify the port: Check BIND environment variable
  3. Check network configuration

Authentication Failed

Error: 401 Unauthorized

Solutions:

  1. Verify token is not expired
  2. Check Authorization header format: Bearer <token>
  3. Confirm authentication is configured correctly
  4. Test with anonymous access (if enabled)

Permission Denied

Error: 403 Forbidden or field returns null

Solutions:

  1. Check user role: Verify JWT claims or API key role
  2. Review access control rules in role_permissions table
  3. Check hidden and disabled flags on permissions for the role
  4. Test with admin role to isolate permission issues

Invalid Query Syntax

Error: Cannot query field 'X' on type 'Y'

Solutions:

  1. Use introspection to check available fields
  2. Verify field names match schema
  3. Check for typos in query

Query Timeout

Error: Request times out

Solutions:

  1. Add limit to reduce result size
  2. Optimize filters and indexes
  3. Check database performance
  4. Consider pagination for large datasets

CORS Errors (Browser)

Error: CORS policy: No 'Access-Control-Allow-Origin' header

Solutions:

  1. Configure CORS environment variables
  2. Check CORS_ALLOWED_ORIGINS includes your domain
  3. Verify CORS_ALLOWED_METHODS includes POST
  4. Ensure CORS_ALLOWED_HEADERS includes Content-Type and Authorization

GraphQL Clients

The GraphQL API works with any standard GraphQL client:

JavaScript/TypeScript

Apollo Client:

import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
uri: 'http://localhost:8080/query',
cache: new InMemoryCache(),
headers: {
authorization: 'Bearer your-token'
}
});

const { data } = await client.query({
query: gql`{ users { id name } }`
});

urql:

import { createClient } from 'urql';

const client = createClient({
url: 'http://localhost:8080/query',
fetchOptions: {
headers: {
authorization: 'Bearer your-token'
}
}
});

graphql-request:

import { GraphQLClient } from 'graphql-request';

const client = new GraphQLClient('http://localhost:8080/query', {
headers: {
authorization: 'Bearer your-token'
}
});

const data = await client.request(`{ users { id name } }`);

Python

hugr-client (Recommended):

from hugr_client import HugrClient

client = HugrClient(
url='http://localhost:8080',
token='your-token'
)

df = client.query('{ users { id name email } }')

See Python Client for more details.

gql:

from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport

transport = RequestsHTTPTransport(
url='http://localhost:8080/query',
headers={'authorization': 'Bearer your-token'}
)

client = Client(transport=transport)

query = gql('{ users { id name } }')
result = client.execute(query)

cURL

curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{"query": "{ users { id name } }"}'

See Also

Documentation

GraphQL Resources