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.
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 introspection for developer tools
- Standards-compliant GraphQL implementation
POST /api/graphql
GET /api/graphql
Both 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)
The GraphQL adapter is included in @objectql/server:
import { createGraphQLHandler } from '@objectql/server';
import { ObjectQL } from '@objectql/core';
const app = new ObjectQL({
datasources: {
default: myDriver
}
});
// Create GraphQL handler
const graphqlHandler = createGraphQLHandler(app);
// Use with your HTTP server
server.on('request', (req, res) => {
if (req.url?.startsWith('/api/graphql')) {
return graphqlHandler(req, res);
}
// ... other handlers
});query {
user(id: "user_123") {
id
name
email
}
}Response:
{
"data": {
"user": {
"id": "user_123",
"name": "Alice",
"email": "alice@example.com"
}
}
}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
# 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
}Query a single record by ID:
query {
user(id: "user_123") {
id
name
email
role
}
}Query multiple records with optional filtering and pagination:
query {
userList(limit: 10, skip: 0) {
id
name
email
}
}Available Arguments:
limit(Int): Maximum number of records to returnskip(Int): Number of records to skip (for pagination)filters(String): JSON-encoded filter expressionfields(List): Specific fields to includesort(String): JSON-encoded sort specification
Use the filters argument with JSON-encoded filter expressions:
query {
userList(
filters: "[[\\"role\\", \\"=\\", \\"admin\\"], \\"and\\", [\\"age\\", \\">=\\", 30]]"
limit: 20
) {
id
name
role
age
}
}Use the sort argument with JSON-encoded sort specification:
query {
userList(
sort: "[[\\"created_at\\", \\"desc\\"]]"
) {
id
name
created_at
}
}GraphQL's field selection naturally limits the data returned:
query {
userList {
id
name
# Only these two fields are returned
}
}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"
}
}
}mutation {
updateUser(id: "user_123", input: {
name: "Alice Updated"
role: "admin"
}) {
id
name
role
updated_at
}
}mutation {
deleteUser(id: "user_123") {
id
deleted
}
}Response:
{
"data": {
"deleteUser": {
"id": "user_123",
"deleted": true
}
}
}GraphQL variables provide a cleaner way to pass dynamic values:
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 CreateUser($input: UserInput!) {
createUser(input: $input) {
id
name
email
}
}Variables:
{
"input": {
"name": "Charlie",
"email": "charlie@example.com",
"role": "user"
}
}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.
Errors follow the GraphQL specification:
{
"errors": [
{
"message": "Object 'nonexistent' not found",
"locations": [{"line": 1, "column": 3}],
"path": ["nonexistent"]
}
],
"data": null
}{
"errors": [
{
"message": "Validation failed",
"extensions": {
"code": "VALIDATION_ERROR"
}
}
]
}{
"data": {
"user": null
}
}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 |
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
}GraphQL provides built-in introspection for schema discovery:
{
__schema {
types {
name
kind
description
}
}
}{
__type(name: "User") {
name
kind
fields {
name
type {
name
kind
}
}
}
}{
__schema {
queryType {
fields {
name
description
}
}
mutationType {
fields {
name
description
}
}
}
}const query = `
query GetUsers {
userList(limit: 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);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
}
}
`
});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>
);
}ObjectQL doesn't include GraphQL Playground by default, but you can easily add it:
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { createGraphQLHandler, generateGraphQLSchema } from '@objectql/server';
const app = express();
const schema = generateGraphQLSchema(objectQLApp);
app.use('/api/graphql', graphqlHTTP({
schema,
graphiql: true // Enable GraphiQL interface
}));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
❌ 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
}
}❌ Don't embed values in queries:
query {
user(id: "user_123") {
name
}
}✅ Do use variables:
query GetUser($id: String!) {
user(id: $id) {
name
}
}fragment UserBasic on User {
id
name
email
}
query {
user(id: "user_123") {
...UserBasic
role
}
userList(limit: 10) {
...UserBasic
}
}query GetUsersPaginated($limit: Int!, $skip: Int!) {
userList(limit: $limit, skip: $skip) {
id
name
email
}
}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
}| 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) |
| 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 |
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
- No Subscriptions: Real-time subscriptions are not yet supported
- 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 Directives: Custom directives not supported
- Nested Relationships: Query related objects without separate requests
- Subscriptions: Real-time updates via WebSocket
- Custom Scalars: Date, DateTime, JSON scalars
- Relay Connections: Standardized pagination
- Field Resolvers: Custom field resolution logic
- DataLoader Integration: Batch and cache database queries
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
- Add Indexes: Index fields used in filters and sorts
- Use Pagination: Always limit result sets
- Optimize Filters: Use indexed fields in filter conditions
GraphQL uses the same authentication as other ObjectQL APIs:
// With JWT
fetch('/api/graphql', {
headers: {
'Authorization': 'Bearer ' + token
}
})ObjectQL's permission system works with GraphQL:
- Object-level permissions
- Field-level permissions
- Record-level permissions
- 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
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
# 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($limit: Int, $skip: Int) {
userList(limit: $limit, 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
}
}Last Updated: January 2026
API Version: 1.0.0