GraphQL API
GraphQL API
ObjectQL provides a GraphQL interface for flexible, efficient queries with complex multi-table relationships. GraphQL allows clients to request exactly the data they need in a single request, making it ideal for modern frontends with complex data requirements.
Overview
The GraphQL API provides:
- Strongly-typed schema automatically generated from ObjectQL metadata
- Single endpoint for all queries and mutations
- Efficient data fetching with precise field selection
- Real-time subscriptions via WebSocket for live data updates
- Apollo Federation support for microservices architecture
- Real-time introspection for developer tools
- Standards-compliant GraphQL implementation
Endpoint
POST /api/graphql
GET /api/graphqlBoth GET and POST methods are supported:
- POST: Send queries in request body (recommended for most cases)
- GET: Send queries via URL parameters (useful for simple queries and caching)
Getting Started
Installation
The GraphQL adapter is available as a protocol plugin @objectql/protocol-graphql:
import { ObjectStackKernel } from '@objectstack/runtime';
import { GraphQLPlugin } from '@objectql/protocol-graphql';
const kernel = new ObjectStackKernel([
myDriver,
new GraphQLPlugin({
port: 4000,
introspection: true,
enableSubscriptions: true, // Enable WebSocket subscriptions
enableFederation: false, // Enable Apollo Federation if needed
federationServiceName: 'objectql'
})
]);
await kernel.start();
// Access GraphQL endpoint: http://localhost:4000/graphql
// WebSocket subscriptions: ws://localhost:4000/graphqlBasic Query Example
query {
user(id: "user_123") {
id
name
email
}
}Response:
{
"data": {
"user": {
"id": "user_123",
"name": "Alice",
"email": "alice@example.com"
}
}
}Schema Generation
The GraphQL schema is automatically generated from your ObjectQL metadata. Each object definition creates:
- Output Type: For query results (e.g.,
User,Task) - Input Type: For mutations (e.g.,
UserInput,TaskInput) - Query Fields: For fetching data (e.g.,
user(id),userList()) - Mutation Fields: For creating/updating/deleting data
Example Object Definition
# user.object.yml
name: user
label: User
fields:
name:
type: text
required: true
email:
type: email
required: true
age:
type: number
role:
type: select
options: [admin, user, guest]Generated GraphQL Types:
type User {
id: String!
name: String!
email: String!
age: Float
role: String
}
input UserInput {
name: String
email: String
age: Float
role: String
}Queries
Fetch Single Record
Query a single record by ID:
query {
user(id: "user_123") {
id
name
email
role
}
}Fetch Multiple Records
Query multiple records with optional filtering and pagination:
query {
userList(top: 10, skip: 0) {
id
name
email
}
}Available Arguments:
top(Int): Maximum number of records to return (LIMIT, aligns with Query Language spec)skip(Int): Number of records to skip (OFFSET, for pagination)filters(String): JSON-encoded filter expression (array format)fields(List): Specific fields to includesort(String): JSON-encoded sort specification
Note: limit is accepted as a deprecated alias for top for backward compatibility.
Advanced Filtering
Use the filters argument with JSON-encoded filter expressions (array format per the Query Language spec):
query {
userList(
filters: "[[\\"role\\", \\"=\\", \\"admin\\"], \\"and\\", [\\"age\\", \\">=\\", 30]]"
top: 20
) {
id
name
role
age
}
}Sorting
Use the sort argument with JSON-encoded sort specification:
query {
userList(
sort: "[[\\"created_at\\", \\"desc\\"]]"
) {
id
name
created_at
}
}Field Selection
GraphQL's field selection naturally limits the data returned:
query {
userList {
id
name
# Only these two fields are returned
}
}Mutations
Create Record
mutation {
createUser(input: {
name: "Bob"
email: "bob@example.com"
role: "user"
}) {
id
name
email
}
}Response:
{
"data": {
"createUser": {
"id": "user_456",
"name": "Bob",
"email": "bob@example.com"
}
}
}Update Record
mutation {
updateUser(id: "user_123", input: {
name: "Alice Updated"
role: "admin"
}) {
id
name
role
updated_at
}
}Delete Record
mutation {
deleteUser(id: "user_123") {
id
deleted
}
}Response:
{
"data": {
"deleteUser": {
"id": "user_123",
"deleted": true
}
}
}Subscriptions
ObjectQL GraphQL supports real-time subscriptions via WebSocket for live data updates. Subscriptions allow clients to receive automatic notifications when data changes.
Enabling Subscriptions
Enable subscriptions in the GraphQL plugin configuration:
new GraphQLPlugin({
port: 4000,
enableSubscriptions: true, // Enable WebSocket support
pubsub: myPubSub // Optional: provide custom PubSub instance
})Available Subscriptions
For each entity, ObjectQL automatically generates three subscription types:
- Created: Notified when a new record is created
- Updated: Notified when a record is updated
- Deleted: Notified when a record is deleted
Subscribe to Created Events
subscription {
userCreated {
id
name
email
role
}
}When a new user is created, subscribers receive:
{
"data": {
"userCreated": {
"id": "user_789",
"name": "New User",
"email": "new@example.com",
"role": "user"
}
}
}Subscribe to Updated Events
subscription {
userUpdated {
id
name
email
updated_at
}
}Subscribe to Deleted Events
subscription {
userDeleted {
id
}
}Filtered Subscriptions
Add where conditions to receive only relevant updates:
subscription {
userCreated(where: { role: "admin" }) {
id
name
email
role
}
}Only users with role = "admin" will trigger notifications.
Client Integration
JavaScript/TypeScript with graphql-ws
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'ws://localhost:4000/graphql',
connectionParams: {
authorization: 'Bearer ' + token
}
});
const subscription = client.subscribe(
{
query: `
subscription {
userCreated {
id
name
email
}
}
`
},
{
next: (data) => {
console.log('New user created:', data);
},
error: (err) => {
console.error('Subscription error:', err);
},
complete: () => {
console.log('Subscription complete');
}
}
);
// Unsubscribe when done
subscription();Apollo Client
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql'
})
);
// Split between HTTP and WebSocket
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});React with Apollo
import { useSubscription, gql } from '@apollo/client';
const USER_CREATED = gql`
subscription OnUserCreated {
userCreated {
id
name
email
}
}
`;
function UserNotifications() {
const { data, loading, error } = useSubscription(USER_CREATED);
if (loading) return <p>Waiting for updates...</p>;
if (error) return <p>Error: {error.message}</p>;
if (data) {
return <p>New user: {data.userCreated.name}</p>;
}
return null;
}Apollo Federation
ObjectQL GraphQL supports Apollo Federation v2 for building distributed GraphQL architectures. This allows you to compose multiple ObjectQL services into a unified supergraph.
Enabling Federation
Enable federation in the GraphQL plugin configuration:
new GraphQLPlugin({
port: 4000,
enableFederation: true,
federationServiceName: 'users-service' // Unique service identifier
})How It Works
When federation is enabled, ObjectQL:
- Generates federated schema with
@keydirectives on entities - Implements reference resolvers for entity resolution
- Exports subgraph schema compatible with Apollo Gateway/Router
Entity Keys
ObjectQL automatically adds @key directives to all entities using their id field:
type User @key(fields: "id") {
id: String!
name: String!
email: String!
}Reference Resolution
ObjectQL implements the _entities resolver required by Federation:
query {
_entities(representations: [{ __typename: "User", id: "user_123" }]) {
... on User {
id
name
email
}
}
}Composing with Apollo Gateway
Create a federated gateway that composes multiple ObjectQL services:
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4000/graphql' },
{ name: 'orders', url: 'http://localhost:4001/graphql' },
{ name: 'products', url: 'http://localhost:4002/graphql' }
]
})
});
const server = new ApolloServer({ gateway });
await server.start();Cross-Service References
Extend entities from other services:
# In orders-service
extend type User @key(fields: "id") {
id: String! @external
orders: [Order!]!
}Best Practices
- Use descriptive service names: Makes debugging easier
- Version your subgraphs: Track schema changes independently
- Monitor gateway health: Watch for subgraph failures
- Test composition locally: Validate schema composition before deployment
Variables
GraphQL variables provide a cleaner way to pass dynamic values:
Query with Variables
query GetUser($userId: String!) {
user(id: $userId) {
id
name
email
}
}Variables:
{
"userId": "user_123"
}Request (POST):
{
"query": "query GetUser($userId: String!) { user(id: $userId) { id name email } }",
"variables": {
"userId": "user_123"
}
}Mutation with Variables
mutation CreateUser($input: UserInput!) {
createUser(input: $input) {
id
name
email
}
}Variables:
{
"input": {
"name": "Charlie",
"email": "charlie@example.com",
"role": "user"
}
}GET Requests
For simple queries, you can use GET requests with URL parameters:
GET /api/graphql?query={user(id:"user_123"){id,name,email}}With variables:
GET /api/graphql?query=query GetUser($id:String!){user(id:$id){name}}&variables={"id":"user_123"}Note: GET requests are useful for:
- Simple queries that can be cached
- Direct browser testing
- Debugging and development
For complex queries or mutations, use POST requests.
Error Handling
GraphQL Errors
Errors follow the GraphQL specification:
{
"errors": [
{
"message": "Object 'nonexistent' not found",
"locations": [{"line": 1, "column": 3}],
"path": ["nonexistent"]
}
],
"data": null
}Validation Errors
{
"errors": [
{
"message": "Validation failed",
"extensions": {
"code": "VALIDATION_ERROR"
}
}
]
}Not Found
{
"data": {
"user": null
}
}Type Mapping
ObjectQL field types are mapped to GraphQL types:
| ObjectQL Type | GraphQL Type | Notes |
|---|---|---|
text, textarea, email, url, phone | String | Text fields |
number, currency, percent | Float | Numeric fields |
auto_number | Int | Integer fields |
boolean | Boolean | True/false |
date, datetime, time | String | ISO 8601 format |
select | String | String enum values |
lookup, master_detail | String | Reference by ID |
file, image | String | File metadata as JSON |
object, json | String | JSON as string |
Required Fields
Fields marked as required: true in ObjectQL become non-nullable (!) in GraphQL:
# ObjectQL
fields:
name:
type: text
required: true# GraphQL
type User {
name: String! # Non-nullable
}Introspection
GraphQL provides built-in introspection for schema discovery:
Get All Types
{
__schema {
types {
name
kind
description
}
}
}Get Type Details
{
__type(name: "User") {
name
kind
fields {
name
type {
name
kind
}
}
}
}Available Operations
{
__schema {
queryType {
fields {
name
description
}
}
mutationType {
fields {
name
description
}
}
}
}Client Integration
JavaScript/TypeScript
const query = `
query GetUsers {
userList(top: 10) {
id
name
email
}
}
`;
const response = await fetch('/api/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ query })
});
const result = await response.json();
console.log(result.data.userList);Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache()
});
const { data } = await client.query({
query: gql`
query GetUsers {
userList {
id
name
email
}
}
`
});React with Apollo
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
userList {
id
name
email
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.userList.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Development Tools
GraphQL Playground
ObjectQL doesn't include GraphQL Playground by default, but you can easily add it:
import { ObjectStackKernel } from '@objectstack/runtime';
import { GraphQLPlugin } from '@objectql/protocol-graphql';
import { ObjectQLPlugin } from '@objectql/core';
import { MemoryDriver } from '@objectql/driver-memory';
const kernel = new ObjectStackKernel([
appConfig,
new MemoryDriver(),
new ObjectQLPlugin(),
new GraphQLPlugin({
port: 4000,
introspection: true
})
]);
await kernel.start();
// GraphQL Playground available via introspection endpointPostman
Import the GraphQL schema into Postman for testing:
- Create a new GraphQL request
- Point to
/api/graphql - Use the introspection feature to load the schema
Best Practices
1. Request Only What You Need
❌ Don't request all fields:
query {
userList {
id
name
email
age
role
created_at
updated_at
# ... many more fields
}
}✅ Do request specific fields:
query {
userList {
id
name
email
}
}2. Use Variables for Dynamic Values
❌ Don't embed values in queries:
query {
user(id: "user_123") {
name
}
}✅ Do use variables:
query GetUser($id: String!) {
user(id: $id) {
name
}
}3. Use Fragments for Reusability
fragment UserBasic on User {
id
name
email
}
query {
user(id: "user_123") {
...UserBasic
role
}
userList(top: 10) {
...UserBasic
}
}4. Implement Pagination
query GetUsersPaginated($top: Int!, $skip: Int!) {
userList(top: $top, skip: $skip) {
id
name
email
}
}5. Handle Errors Gracefully
const result = await fetch('/api/graphql', {
method: 'POST',
body: JSON.stringify({ query })
});
const json = await result.json();
if (json.errors) {
console.error('GraphQL errors:', json.errors);
// Handle errors appropriately
}
if (json.data) {
// Process data
}Comparison with Other APIs
GraphQL vs REST
| Feature | GraphQL | REST |
|---|---|---|
| Endpoint | Single endpoint | Multiple endpoints |
| Data Fetching | Precise field selection | Fixed response structure |
| Multiple Resources | Single request | Multiple requests |
| Over-fetching | No | Common |
| Under-fetching | No | Common |
| Versioning | Schema evolution | URL versioning |
| Caching | More complex | Simple (HTTP) |
GraphQL vs JSON-RPC
| Feature | GraphQL | JSON-RPC |
|---|---|---|
| Type System | Strongly typed | Flexible |
| Introspection | Built-in | Not available |
| Field Selection | Granular | All or custom |
| Developer Tools | Excellent | Limited |
| Learning Curve | Moderate | Low |
| Flexibility | High | Very High |
When to Use GraphQL
Use GraphQL when:
- Building complex UIs with nested data requirements
- Client needs flexibility in data fetching
- You want strong typing and introspection
- Reducing network requests is critical
- Working with modern frontend frameworks (React, Vue, Angular)
Use REST when:
- Simple CRUD operations
- Caching is critical
- Working with legacy systems
- Team is more familiar with REST
Use JSON-RPC when:
- Need maximum flexibility
- Building internal microservices
- Working with AI agents
- Custom operations beyond CRUD
Limitations
Current Limitations
- No Nested Mutations: Cannot create related records in a single mutation
- Basic Relationships: Relationships are represented as IDs, not nested objects
- No Custom Scalars: Uses built-in GraphQL scalars only
- No Custom Directives: User-defined directives not supported
Planned Features
- Nested Relationships: Query related objects without separate requests
- Custom Scalars: Date, DateTime, JSON scalars
- Relay Connections: Standardized pagination with cursor-based pagination
- Field Resolvers: Custom field resolution logic
- Query Complexity Analysis: Automated query cost calculation
Performance Considerations
Query Complexity
ObjectQL GraphQL doesn't currently limit query complexity. For production:
- Implement Rate Limiting: Limit requests per user/IP
- Set Depth Limits: Prevent deeply nested queries
- Monitor Performance: Track slow queries
- Add Caching: Use Redis or similar for frequently accessed data
Database Optimization
- Add Indexes: Index fields used in filters and sorts
- Use Pagination: Always limit result sets
- Optimize Filters: Use indexed fields in filter conditions
Security
Authentication
GraphQL uses the same authentication as other ObjectQL APIs:
// With JWT
fetch('/api/graphql', {
headers: {
'Authorization': 'Bearer ' + token
}
})Authorization
ObjectQL's permission system works with GraphQL:
- Object-level permissions
- Field-level permissions
- Record-level permissions
Best Practices
- Always Authenticate: Require authentication for mutations
- Validate Input: ObjectQL validates based on schema
- Rate Limit: Prevent abuse
- Sanitize Errors: Don't expose internal details in production
- Use HTTPS: Always in production
Troubleshooting
Common Issues
Query Returns Null
Check that:
- Object exists in metadata
- ID is correct
- User has permission
- Record exists in database
Type Errors
Ensure:
- Variable types match schema
- Required fields are provided
- Field names are correct
Performance Issues
Solutions:
- Limit result sets with pagination
- Request only needed fields
- Add database indexes
- Use caching
Examples
Complete CRUD Example
# Create
mutation CreateUser($input: UserInput!) {
createUser(input: $input) {
id
name
email
}
}
# Read One
query GetUser($id: String!) {
user(id: $id) {
id
name
email
role
}
}
# Read Many
query ListUsers($top: Int, $skip: Int) {
userList(top: $top, skip: $skip) {
id
name
email
}
}
# Update
mutation UpdateUser($id: String!, $input: UserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
updated_at
}
}
# Delete
mutation DeleteUser($id: String!) {
deleteUser(id: $id) {
id
deleted
}
}Further Reading
Last Updated: February 2026
API Version: 1.0.0
New in v4.0.3: WebSocket Subscriptions, Apollo Federation v2 support