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
| Code | Description |
|---|---|
200 OK | Request processed (may contain GraphQL errors) |
400 Bad Request | Malformed GraphQL query or invalid JSON |
401 Unauthorized | Missing or invalid authentication |
403 Forbidden | Access denied by authorization rules |
500 Internal Server Error | Server-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 } }"}'
Cookie Authentication
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:
- Check hugr is running:
curl http://localhost:8080/query - Verify the port: Check
BINDenvironment variable - Check network configuration
Authentication Failed
Error: 401 Unauthorized
Solutions:
- Verify token is not expired
- Check Authorization header format:
Bearer <token> - Confirm authentication is configured correctly
- Test with anonymous access (if enabled)
Permission Denied
Error: 403 Forbidden or field returns null
Solutions:
- Check user role: Verify JWT claims or API key role
- Review access control rules in
role_permissionstable - Check
hiddenanddisabledflags on permissions for the role - Test with admin role to isolate permission issues
Invalid Query Syntax
Error: Cannot query field 'X' on type 'Y'
Solutions:
- Use introspection to check available fields
- Verify field names match schema
- Check for typos in query
Query Timeout
Error: Request times out
Solutions:
- Add
limitto reduce result size - Optimize filters and indexes
- Check database performance
- Consider pagination for large datasets
CORS Errors (Browser)
Error: CORS policy: No 'Access-Control-Allow-Origin' header
Solutions:
- Configure CORS environment variables
- Check
CORS_ALLOWED_ORIGINSincludes your domain - Verify
CORS_ALLOWED_METHODSincludes POST - Ensure
CORS_ALLOWED_HEADERSincludesContent-TypeandAuthorization
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
- Admin UI - GraphiQL interface for the GraphQL API
- JQ Query Endpoint - REST endpoint with JQ transformations
- Python Client - Python library for hugr
- Hugr IPC - Efficient binary protocol for analytics
- Authentication Setup - Configure authentication methods
- Access Control - Role-based permissions
- Cache Directives - Query result caching