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:
- Production: playground.appmint.io
- Staging: staging-playground.appmint.io
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
-
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 } } } } }
-
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 } }
-
Implement Proper Error Handling
mutation CreatePost($input: CreateDocumentInput!) { createDocument(input: $input) { document { id data } errors { field message code } } }
Security Considerations
-
Validate Input Data
const createPostInput = { collection: 'posts', data: { title: sanitizeHtml(rawTitle), content: sanitizeHtml(rawContent), authorId: authenticatedUserId // Use authenticated user } };
-
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 } });
-
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
-
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' } });
-
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 →