GraphQL API

Complete guide to using Appmint's GraphQL API for flexible and efficient data querying

Appmint Developer Relations1/15/202422 min read

GraphQL API

Appmint's GraphQL API provides a flexible, efficient way to query and manipulate your data. With GraphQL, you can fetch exactly the data you need in a single request, reduce over-fetching, and build faster applications with better performance.

Why GraphQL?

Advantages Over REST

Single Endpoint:

  • One URL for all operations: https://api.appmint.io/graphql
  • No need to manage multiple endpoints
  • Simplified API versioning

Precise Data Fetching:

  • Request exactly the fields you need
  • Eliminate over-fetching and under-fetching
  • Reduce bandwidth usage

Strongly Typed:

  • Full schema introspection
  • Excellent tooling support
  • Compile-time validation

Real-time Subscriptions:

  • Built-in subscription support
  • Live data updates
  • Efficient change notifications

Getting Started

GraphQL Endpoint

Production: https://api.appmint.io/graphql Staging: https://staging-api.appmint.io/graphql

Authentication

POST https://api.appmint.io/graphql
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
X-Appmint-Project-ID: YOUR_PROJECT_ID

{
  "query": "query { viewer { id email } }"
}

GraphQL Playground

Explore the API interactively:


Schema Overview

Core Types

# User Management
type User {
  id: ID!
  email: String!
  name: String
  profile: UserProfile
  createdAt: DateTime!
  updatedAt: DateTime!
}

type UserProfile {
  firstName: String
  lastName: String
  avatar: String
  bio: String
  timezone: String
  locale: String
}

# Database Collections
type Collection {
  id: ID!
  name: String!
  schema: JSONSchema
  documents(
    filter: JSONObject
    sort: [SortInput]
    limit: Int = 20
    offset: Int = 0
  ): DocumentConnection!
}

type Document {
  id: ID!
  collection: String!
  data: JSONObject!
  createdAt: DateTime!
  updatedAt: DateTime!
  version: Int!
}

# File Storage
type File {
  id: ID!
  name: String!
  path: String!
  size: Int!
  mimeType: String!
  url: String!
  thumbnails: [Thumbnail]
  metadata: FileMetadata
  createdAt: DateTime!
}

# Workflow Management
type Workflow {
  id: ID!
  name: String!
  status: WorkflowStatus!
  steps: [WorkflowStep]!
  variables: JSONObject
  createdAt: DateTime!
}

enum WorkflowStatus {
  DRAFT
  ACTIVE
  PAUSED
  COMPLETED
  FAILED
}

Scalar Types

scalar DateTime    # ISO 8601 date-time string
scalar JSONObject  # Arbitrary JSON object
scalar JSONSchema  # JSON Schema specification
scalar Upload      # File upload

Queries

Basic Queries

Fetch Current User

query GetViewer {
  viewer {
    id
    email
    name
    profile {
      firstName
      lastName
      avatar
    }
    createdAt
  }
}

Query Documents

query GetPosts($filter: JSONObject, $limit: Int) {
  collection(name: "posts") {
    documents(filter: $filter, limit: $limit) {
      edges {
        node {
          id
          data
          createdAt
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      totalCount
    }
  }
}

Variables:

{
  "filter": { "published": true },
  "limit": 10
}

Complex Filtering

query GetFilteredPosts {
  collection(name: "posts") {
    documents(
      filter: {
        published: true
        tags: { $in: ["technology", "startup"] }
        views: { $gte: 100 }
        createdAt: { $gte: "2024-01-01T00:00:00Z" }
      }
      sort: [
        { field: "views", order: DESC }
        { field: "createdAt", order: DESC }
      ]
      limit: 20
    ) {
      edges {
        node {
          id
          data
          createdAt
        }
      }
    }
  }
}

Nested Queries

Users with Their Posts

query GetUsersWithPosts {
  users(limit: 10) {
    edges {
      node {
        id
        email
        name
        posts: relatedDocuments(
          collection: "posts"
          field: "authorId"
        ) {
          edges {
            node {
              id
              data
            }
          }
        }
      }
    }
  }
}

Posts with Authors and Comments

query GetPostsWithDetails($postId: ID!) {
  document(collection: "posts", id: $postId) {
    id
    data
    author: relatedDocument(
      collection: "users"
      field: "authorId"
    ) {
      id
      email
      name
    }
    comments: relatedDocuments(
      collection: "comments"
      field: "postId"
    ) {
      edges {
        node {
          id
          data
          author: relatedDocument(
            collection: "users"
            field: "authorId"
          ) {
            name
          }
        }
      }
    }
  }
}

Pagination

Cursor-Based Pagination

query GetPostsPaginated($after: String, $first: Int = 10) {
  collection(name: "posts") {
    documents(after: $after, first: $first) {
      edges {
        cursor
        node {
          id
          data
        }
      }
      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
    }
  }
}

Load More Posts

// JavaScript pagination example
let cursor = null;
const posts = [];

async function loadMorePosts() {
  const response = await graphqlClient.query({
    query: GET_POSTS_PAGINATED,
    variables: {
      after: cursor,
      first: 10
    }
  });
  
  const { edges, pageInfo } = response.data.collection.documents;
  
  // Add new posts to existing list
  posts.push(...edges.map(edge => edge.node));
  
  // Update cursor for next page
  cursor = pageInfo.endCursor;
  
  // Check if more pages available
  return pageInfo.hasNextPage;
}

Mutations

Create Operations

Create User

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    user {
      id
      email
      name
      profile {
        firstName
        lastName
      }
    }
    errors {
      field
      message
    }
  }
}

Variables:

{
  "input": {
    "email": "john@example.com",
    "password": "securePassword123",
    "name": "John Doe",
    "profile": {
      "firstName": "John",
      "lastName": "Doe",
      "timezone": "America/New_York"
    },
    "metadata": {
      "source": "signup_form"
    }
  }
}

Create Document

mutation CreatePost($input: CreateDocumentInput!) {
  createDocument(input: $input) {
    document {
      id
      data
      createdAt
    }
    errors {
      field
      message
    }
  }
}

Variables:

{
  "input": {
    "collection": "posts",
    "data": {
      "title": "Getting Started with GraphQL",
      "content": "GraphQL is a powerful query language...",
      "authorId": "user_123",
      "published": false,
      "tags": ["graphql", "tutorial"]
    }
  }
}

Update Operations

Update Document

mutation UpdatePost($id: ID!, $input: UpdateDocumentInput!) {
  updateDocument(id: $id, input: $input) {
    document {
      id
      data
      updatedAt
      version
    }
    errors {
      field
      message
    }
  }
}

Variables:

{
  "id": "doc_abc123",
  "input": {
    "collection": "posts",
    "data": {
      "title": "Updated: Getting Started with GraphQL",
      "published": true,
      "publishedAt": "2024-01-15T10:30:00Z"
    }
  }
}

Bulk Update

mutation BulkUpdatePosts($filter: JSONObject!, $update: JSONObject!) {
  bulkUpdateDocuments(
    collection: "posts"
    filter: $filter
    update: $update
  ) {
    modifiedCount
    matchedCount
    errors {
      field
      message
    }
  }
}

Variables:

{
  "filter": { "authorId": "user_123", "published": false },
  "update": { "published": true, "publishedAt": "2024-01-15T10:30:00Z" }
}

Delete Operations

Delete Document

mutation DeletePost($id: ID!) {
  deleteDocument(collection: "posts", id: $id) {
    success
    errors {
      field
      message
    }
  }
}

Bulk Delete

mutation BulkDeletePosts($filter: JSONObject!) {
  bulkDeleteDocuments(collection: "posts", filter: $filter) {
    deletedCount
    errors {
      field
      message
    }
  }
}

File Operations

Upload File

mutation UploadFile($input: UploadFileInput!) {
  uploadFile(input: $input) {
    file {
      id
      name
      path
      url
      size
      mimeType
    }
    errors {
      field
      message
    }
  }
}

Variables (multipart/form-data):

{
  "variables": {
    "input": {
      "file": null,
      "path": "uploads/images/",
      "public": true,
      "metadata": {
        "alt": "Profile picture",
        "category": "avatar"
      }
    }
  },
  "map": {
    "0": ["variables.input.file"]
  }
}

Subscriptions

Real-time Updates

Subscribe to Document Changes

subscription DocumentUpdates($collection: String!, $filter: JSONObject) {
  documentUpdated(collection: $collection, filter: $filter) {
    mutation # CREATED, UPDATED, DELETED
    document {
      id
      data
      updatedAt
    }
    previousData # For update operations
  }
}

Variables:

{
  "collection": "posts",
  "filter": { "published": true }
}

Subscribe to User Activities

subscription UserActivities($userId: ID!) {
  userActivity(userId: $userId) {
    type # LOGIN, LOGOUT, PROFILE_UPDATE
    user {
      id
      name
      lastActiveAt
    }
    metadata
    timestamp
  }
}

WebSocket Connection

// Using Apollo Client with subscriptions
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({
  uri: 'https://api.appmint.io/graphql',
  headers: {
    authorization: `Bearer ${token}`,
    'x-appmint-project-id': projectId
  }
});

const wsLink = new GraphQLWsLink(createClient({
  url: 'wss://api.appmint.io/graphql',
  connectionParams: {
    authorization: `Bearer ${token}`,
    'x-appmint-project-id': projectId
  }
}));

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()
});

// Subscribe to real-time updates
const subscription = client.subscribe({
  query: DOCUMENT_UPDATES_SUBSCRIPTION,
  variables: { collection: 'posts' }
}).subscribe({
  next: (result) => {
    console.log('Document update:', result.data.documentUpdated);
  },
  error: (err) => {
    console.error('Subscription error:', err);
  }
});

Advanced Features

Batch Operations

Multiple Mutations in Single Request

mutation BatchOperations(
  $createUserInput: CreateUserInput!
  $createPostInput: CreateDocumentInput!
) {
  createUser: createUser(input: $createUserInput) {
    user {
      id
      email
    }
    errors {
      field
      message
    }
  }
  
  createPost: createDocument(input: $createPostInput) {
    document {
      id
      data
    }
    errors {
      field
      message
    }
  }
}

Fragments

Reusable Field Sets

# Fragment definition
fragment UserInfo on User {
  id
  email
  name
  profile {
    firstName
    lastName
    avatar
  }
}

fragment PostInfo on Document {
  id
  data
  createdAt
  updatedAt
}

# Using fragments
query GetPostsWithAuthors {
  collection(name: "posts") {
    documents(limit: 10) {
      edges {
        node {
          ...PostInfo
          author: relatedDocument(
            collection: "users"
            field: "authorId"
          ) {
            ...UserInfo
          }
        }
      }
    }
  }
}

Directives

Conditional Fields

query GetPost($id: ID!, $includeComments: Boolean = false) {
  document(collection: "posts", id: $id) {
    id
    data
    comments: relatedDocuments(
      collection: "comments"
      field: "postId"
    ) @include(if: $includeComments) {
      edges {
        node {
          id
          data
        }
      }
    }
  }
}

Skip Fields

query GetUser($id: ID!, $skipProfile: Boolean = false) {
  user(id: $id) {
    id
    email
    profile @skip(if: $skipProfile) {
      firstName
      lastName
    }
  }
}

Error Handling

Error Types

type Error {
  field: String
  message: String!
  code: String!
  path: [String]
}

# Common error codes
enum ErrorCode {
  VALIDATION_ERROR
  AUTHENTICATION_ERROR
  AUTHORIZATION_ERROR
  NOT_FOUND
  RATE_LIMIT_EXCEEDED
  INTERNAL_SERVER_ERROR
}

Handling Errors in Responses

// JavaScript error handling
async function createPost(postData) {
  try {
    const response = await graphqlClient.mutate({
      mutation: CREATE_POST_MUTATION,
      variables: { input: postData }
    });
    
    const { document, errors } = response.data.createDocument;
    
    if (errors && errors.length > 0) {
      // Handle validation errors
      const validationErrors = {};
      errors.forEach(error => {
        validationErrors[error.field] = error.message;
      });
      throw new ValidationError(validationErrors);
    }
    
    return document;
  } catch (error) {
    if (error.networkError) {
      console.error('Network error:', error.networkError);
    }
    
    if (error.graphQLErrors) {
      error.graphQLErrors.forEach(err => {
        console.error('GraphQL error:', err.message);
      });
    }
    
    throw error;
  }
}

Global Error Handling

// Apollo Client error handling
import { onError } from '@apollo/client/link/error';

const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
      console.error(
        `GraphQL error: Message: ${message}, Location: ${locations}, Path: ${path}`
      );
      
      // Handle specific error types
      if (extensions?.code === 'AUTHENTICATION_ERROR') {
        // Redirect to login
        window.location.href = '/login';
      }
    });
  }
  
  if (networkError) {
    console.error(`Network error: ${networkError}`);
    
    // Retry on network errors
    if (networkError.statusCode === 500) {
      return forward(operation);
    }
  }
});

const client = new ApolloClient({
  link: from([errorLink, httpLink]),
  cache: new InMemoryCache()
});

Performance Optimization

Query Complexity Analysis

Appmint automatically analyzes query complexity to prevent expensive operations:

# This query might be rejected due to high complexity
query ExpensiveQuery {
  users(limit: 1000) {  # High limit
    edges {
      node {
        posts(limit: 100) {  # Nested high limit
          edges {
            node {
              comments(limit: 50) {  # Triple nested
                edges {
                  node {
                    id
                    data
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Query Cost Limits

// Query cost is calculated based on:
// - Field complexity
// - Nested depth
// - List size multipliers
// - Custom field costs

// Maximum query cost: 1000 points
// Exceeded queries return error:
{
  "errors": [{
    "message": "Query cost 1250 exceeds maximum cost 1000",
    "extensions": {
      "code": "MAX_QUERY_COMPLEXITY_EXCEEDED",
      "cost": 1250,
      "maximum": 1000
    }
  }]
}

Query Optimization Tips

Use Fragments for Repeated Fields

# Instead of repeating fields
query BadExample {
  user1: user(id: "1") {
    id
    email
    name
    profile {
      firstName
      lastName
    }
  }
  user2: user(id: "2") {
    id
    email
    name
    profile {
      firstName
      lastName
    }
  }
}

# Use fragments
fragment UserDetails on User {
  id
  email
  name
  profile {
    firstName
    lastName
  }
}

query GoodExample {
  user1: user(id: "1") {
    ...UserDetails
  }
  user2: user(id: "2") {
    ...UserDetails
  }
}

Implement Proper Pagination

# Don't fetch all data at once
query BadPagination {
  collection(name: "posts") {
    documents(limit: 10000) {  # Too many items
      edges {
        node {
          id
          data
        }
      }
    }
  }
}

# Use reasonable page sizes
query GoodPagination($after: String) {
  collection(name: "posts") {
    documents(first: 20, after: $after) {  # Reasonable page size
      edges {
        cursor
        node {
          id
          data
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

Caching Strategies

Client-Side Caching

// Apollo Client caching
import { InMemoryCache } from '@apollo/client';

const cache = new InMemoryCache({
  typePolicies: {
    Document: {
      keyFields: ['collection', 'id'],
    },
    Collection: {
      fields: {
        documents: {
          keyArgs: ['filter', 'sort'],
          merge(existing = { edges: [] }, incoming) {
            return {
              ...incoming,
              edges: [...existing.edges, ...incoming.edges],
            };
          },
        },
      },
    },
  },
});

// Cache-first query policy
const { data, loading, error } = useQuery(GET_POSTS_QUERY, {
  fetchPolicy: 'cache-first', // Use cache if available
  nextFetchPolicy: 'cache-only', // Don't refetch after first load
});

Query Deduplication

// Automatic query deduplication
const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'ignore',
    },
    query: {
      errorPolicy: 'all',
    },
  },
});

// Multiple identical queries will be deduplicated
Promise.all([
  client.query({ query: GET_USER_QUERY, variables: { id: '1' } }),
  client.query({ query: GET_USER_QUERY, variables: { id: '1' } }),
  client.query({ query: GET_USER_QUERY, variables: { id: '1' } })
]);
// Only one network request is made

Testing GraphQL Operations

Unit Testing Queries

import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';

const GET_USER_QUERY = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

const mocks = [
  {
    request: {
      query: GET_USER_QUERY,
      variables: { id: '1' },
    },
    result: {
      data: {
        user: {
          id: '1',
          name: 'John Doe',
          email: 'john@example.com',
        },
      },
    },
  },
];

test('renders user data', async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <UserProfile userId="1" />
    </MockedProvider>
  );

  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });
});

Integration Testing

// Test actual GraphQL endpoint
import { createTestClient } from 'apollo-server-testing';

const { query, mutate } = createTestClient(server);

test('creates user successfully', async () => {
  const CREATE_USER_MUTATION = gql`
    mutation CreateUser($input: CreateUserInput!) {
      createUser(input: $input) {
        user {
          id
          email
        }
        errors {
          field
          message
        }
      }
    }
  `;

  const response = await mutate({
    mutation: CREATE_USER_MUTATION,
    variables: {
      input: {
        email: 'test@example.com',
        password: 'password123',
        name: 'Test User'
      }
    }
  });

  expect(response.data.createUser.user).toBeDefined();
  expect(response.data.createUser.user.email).toBe('test@example.com');
  expect(response.data.createUser.errors).toHaveLength(0);
});

Best Practices

Query Design

  1. Request Only Needed Fields

    # Good: Minimal fields
    query GetPosts {
      collection(name: "posts") {
        documents {
          edges {
            node {
              id
              data
            }
          }
        }
      }
    }
    
    # Bad: Unnecessary fields
    query GetPosts {
      collection(name: "posts") {
        documents {
          edges {
            node {
              id
              data
              createdAt
              updatedAt
              version
              # ... many more fields
            }
          }
        }
      }
    }
    
  2. Use Variables for Dynamic Values

    # Good: Using variables
    query GetPost($id: ID!) {
      document(collection: "posts", id: $id) {
        id
        data
      }
    }
    
    # Bad: Hardcoded values
    query GetPost {
      document(collection: "posts", id: "post_123") {
        id
        data
      }
    }
    
  3. Implement Proper Error Handling

    mutation CreatePost($input: CreateDocumentInput!) {
      createDocument(input: $input) {
        document {
          id
          data
        }
        errors {
          field
          message
          code
        }
      }
    }
    

Security Considerations

  1. Validate Input Data

    const createPostInput = {
      collection: 'posts',
      data: {
        title: sanitizeHtml(rawTitle),
        content: sanitizeHtml(rawContent),
        authorId: authenticatedUserId // Use authenticated user
      }
    };
    
  2. Use Proper Authentication

    // Always include authentication headers
    const client = new ApolloClient({
      uri: 'https://api.appmint.io/graphql',
      headers: {
        authorization: `Bearer ${getAuthToken()}`,
        'x-appmint-project-id': PROJECT_ID
      }
    });
    
  3. Implement Rate Limiting

    // Handle rate limit errors gracefully
    const errorLink = onError(({ graphQLErrors }) => {
      graphQLErrors?.forEach((err) => {
        if (err.extensions?.code === 'RATE_LIMIT_EXCEEDED') {
          // Wait before retrying
          setTimeout(() => {
            // Retry operation
          }, err.extensions.retryAfter * 1000);
        }
      });
    });
    

GraphQL Tools & Libraries

Recommended Client Libraries

JavaScript/TypeScript:

  • Apollo Client - Full-featured GraphQL client
  • Relay - Facebook's GraphQL client
  • urql - Lightweight alternative
  • graphql-request - Minimal GraphQL client

Python:

  • GQL - GraphQL client library
  • python-graphql-client - Simple GraphQL client
  • Strawberry - Modern GraphQL library

Java:

  • Apollo Android - Android GraphQL client
  • graphql-java-client - Java GraphQL client

Development Tools

GraphQL Playground:

  • Interactive query explorer
  • Schema documentation
  • Query validation
  • Subscription testing

GraphiQL:

  • In-browser GraphQL IDE
  • Auto-completion
  • Query history
  • Schema explorer

Apollo Studio:

  • Schema management
  • Performance monitoring
  • Query analytics
  • Error tracking

Schema Introspection

Explore Available Types

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

Get Query Root Fields

query GetQueryType {
  __schema {
    queryType {
      fields {
        name
        description
        args {
          name
          type {
            name
          }
        }
      }
    }
  }
}

Migration from REST

REST vs GraphQL Comparison

REST GraphQL
Multiple endpoints Single endpoint
Over/under-fetching Precise data fetching
Multiple requests Single request
Server-defined responses Client-defined responses
Caching by URL Caching by query

Migration Strategy

  1. Start with Simple Queries

    // REST
    const user = await fetch('/api/users/123').then(r => r.json());
    const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json());
    
    // GraphQL
    const { user } = await graphqlClient.query({
      query: gql`
        query GetUserWithPosts($id: ID!) {
          user(id: $id) {
            id
            name
            posts {
              edges {
                node {
                  id
                  data
                }
              }
            }
          }
        }
      `,
      variables: { id: '123' }
    });
    
  2. Implement Gradually

    • Use GraphQL for new features
    • Migrate high-traffic endpoints first
    • Keep REST endpoints for legacy clients
    • Use GraphQL federation for microservices

Ready to start using GraphQL? Explore our interactive playground and build more efficient applications with precise data fetching.

Open GraphQL Playground → View Complete Schema → Join GraphQL Community →