Skip to content
Last updated: 2026-04-06
Reference

Advanced Integration Patterns

Production-ready patterns for Dxtra API integration including error handling, batch operations, and event-driven workflows.

Robust Client Implementation

A production-ready GraphQL client with automatic token refresh, retry logic, and error handling.

Node.js / TypeScript

TypeScript
import { GraphQLClient, ClientError } from 'graphql-request';

interface AuthSession {
  accessToken: string;
  refreshToken: string;
  accessTokenExpiresIn: number;
}

interface DxtraConfig {
  personalAccessToken: string;
  authUrl?: string;
  apiUrl?: string;
  maxRetries?: number;
  retryDelayMs?: number;
}

class DxtraClient {
  private config: Required<DxtraConfig>;
  private client: GraphQLClient | null = null;
  private tokenExpiry = 0;

  constructor(config: DxtraConfig) {
    this.config = {
      authUrl: 'https://auth.dxtra.ai',
      apiUrl: 'https://api.dxtra.ai/v1/graphql',
      maxRetries: 3,
      retryDelayMs: 1000,
      ...config
    };
  }

  private async refreshToken(): Promise<void> {
    const response = await fetch(`${this.config.authUrl}/v1/signin/pat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        personalAccessToken: this.config.personalAccessToken
      })
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Authentication failed: ${response.status} - ${error}`);
    }

    const data: { session: AuthSession } = await response.json();

    this.client = new GraphQLClient(this.config.apiUrl, {
      headers: {
        'Authorization': `Bearer ${data.session.accessToken}`,
        'X-Hasura-Role': 'user'
      }
    });

    // Refresh 5 minutes before expiration
    this.tokenExpiry = Date.now() + (data.session.accessTokenExpiresIn - 300) * 1000;
  }

  private async getClient(): Promise<GraphQLClient> {
    if (!this.client || Date.now() >= this.tokenExpiry) {
      await this.refreshToken();
    }
    return this.client!;
  }

  async request<T>(
    query: string,
    variables?: Record<string, unknown>
  ): Promise<T> {
    let lastError: Error | null = null;

    for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
      try {
        const client = await this.getClient();
        return await client.request<T>(query, variables);
      } catch (error) {
        lastError = error as Error;

        // Handle specific error types
        if (error instanceof ClientError) {
          // Authentication error - force token refresh
          if (error.response.status === 401) {
            this.client = null;
            this.tokenExpiry = 0;
            continue;
          }

          // Rate limit - exponential backoff
          if (error.response.status === 429) {
            const delay = this.config.retryDelayMs * Math.pow(2, attempt);
            await this.sleep(delay);
            continue;
          }

          // GraphQL errors - don't retry
          if (error.response.errors) {
            throw error;
          }
        }

        // Network errors - retry with backoff
        const delay = this.config.retryDelayMs * Math.pow(2, attempt);
        await this.sleep(delay);
      }
    }

    throw lastError || new Error('Request failed after retries');
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const client = new DxtraClient({
  personalAccessToken: process.env.DXTRA_PAT!
});

const data = await client.request<{ dataControllers: Array<{ id: string; title: string }> }>(`
  query {
    dataControllers {
      id
      title
    }
  }
`);

Python

Python
import time
import requests
from typing import Dict, Any, Optional
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.exceptions import TransportQueryError

class DxtraClient:
    def __init__(
        self,
        personal_access_token: str,
        auth_url: str = 'https://auth.dxtra.ai',
        api_url: str = 'https://api.dxtra.ai/v1/graphql',
        max_retries: int = 3,
        retry_delay: float = 1.0
    ):
        self.pat = personal_access_token
        self.auth_url = auth_url
        self.api_url = api_url
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        self._client: Optional[Client] = None
        self._token_expiry = 0

    def _refresh_token(self) -> None:
        """Exchange PAT for JWT token."""
        response = requests.post(
            f'{self.auth_url}/v1/signin/pat',
            json={'personalAccessToken': self.pat}
        )

        if not response.ok:
            raise Exception(f'Authentication failed: {response.status_code} - {response.text}')

        session = response.json()['session']

        transport = RequestsHTTPTransport(
            url=self.api_url,
            headers={
                'Authorization': f'Bearer {session["accessToken"]}',
                'X-Hasura-Role': 'user'
            }
        )

        self._client = Client(transport=transport, fetch_schema_from_transport=True)

        # Refresh 5 minutes before expiration
        self._token_expiry = time.time() + session['accessTokenExpiresIn'] - 300

    def _get_client(self) -> Client:
        """Get authenticated client, refreshing token if needed."""
        if self._client is None or time.time() >= self._token_expiry:
            self._refresh_token()
        return self._client

    def request(self, query_string: str, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Execute GraphQL request with retry logic."""
        last_error = None
        query = gql(query_string)

        for attempt in range(self.max_retries):
            try:
                client = self._get_client()
                return client.execute(query, variable_values=variables)

            except TransportQueryError as e:
                last_error = e

                # Authentication error - force refresh
                if hasattr(e, 'errors') and any('unauthorized' in str(err).lower() for err in e.errors):
                    self._client = None
                    self._token_expiry = 0
                    continue

                # Don't retry GraphQL errors
                raise

            except requests.exceptions.RequestException as e:
                last_error = e
                # Network error - retry with backoff
                delay = self.retry_delay * (2 ** attempt)
                time.sleep(delay)

        raise last_error or Exception('Request failed after retries')


# Usage
client = DxtraClient(os.getenv('DXTRA_PAT'))

result = client.request("""
    query {
        dataControllers {
            id
            title
        }
    }
""")

Batch Operations

Batch Query Pattern

Execute multiple related queries efficiently.

JavaScript
// Fetch multiple data types in a single request
const batchQuery = `
  query BatchFetch($controllerId: uuid!, $limit: Int = 50) {
    dataController(id: $controllerId) {
      id
      title
      did
    }

    dataSubjects(
      where: { dataControllerId: { _eq: $controllerId } }
      limit: $limit
    ) {
      id
      did
      createdAt
    }

    dataSubjectRightsRequests(
      where: { dataSubject: { dataControllerId: { _eq: $controllerId } } }
      limit: $limit
      orderBy: { createdAt: desc }
    ) {
      id
      requestType
      status
      createdAt
    }

    dataSubjectConsentFormValuesAggregate(
      where: { dataSubject: { dataControllerId: { _eq: $controllerId } } }
    ) {
      aggregate {
        count
      }
    }
  }
`;

const result = await client.request(batchQuery, {
  controllerId: 'your-controller-uuid',
  limit: 100
});

Pagination Pattern

Handle large datasets with cursor-based pagination.

JavaScript
async function* paginatedQuery(client, controllerId, pageSize = 100) {
  let offset = 0;
  let hasMore = true;

  while (hasMore) {
    const query = `
      query GetDataSubjects($controllerId: uuid!, $limit: Int!, $offset: Int!) {
        dataSubjects(
          where: { dataControllerId: { _eq: $controllerId } }
          limit: $limit
          offset: $offset
          orderBy: { createdAt: asc }
        ) {
          id
          did
          createdAt
          updatedAt
        }
        dataSubjectsAggregate(
          where: { dataControllerId: { _eq: $controllerId } }
        ) {
          aggregate {
            count
          }
        }
      }
    `;

    const result = await client.request(query, {
      controllerId,
      limit: pageSize,
      offset
    });

    const subjects = result.dataSubjects;
    const totalCount = result.dataSubjectsAggregate.aggregate.count;

    if (subjects.length > 0) {
      yield subjects;
    }

    offset += pageSize;
    hasMore = offset < totalCount;
  }
}

// Usage
for await (const batch of paginatedQuery(client, 'controller-uuid')) {
  console.log(`Processing ${batch.length} data subjects`);
  // Process batch...
}

Event-Driven Integration

Webhook Handler with Validation

Secure webhook handler with signature validation and idempotency.

JavaScript
import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

// Track processed events for idempotency
const processedEvents = new Set();

function verifyWebhookSignature(req, secret) {
  const signature = req.headers['x-dxtra-signature'];
  if (!signature) return false;

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(req.rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhooks/dxtra', async (req, res) => {
  // Validate signature
  const webhookSecret = process.env.DXTRA_WEBHOOK_SECRET;
  if (webhookSecret && !verifyWebhookSignature(req, webhookSecret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { event, eventId, data, timestamp } = req.body;

  // Idempotency check
  if (eventId && processedEvents.has(eventId)) {
    return res.status(200).json({ status: 'already_processed' });
  }

  try {
    // Process event based on type
    switch (event) {
      case 'consent.updated':
        await handleConsentUpdate(data);
        break;

      case 'rights_request.created':
        await handleRightsRequest(data);
        break;

      case 'data_subject.created':
        await handleNewDataSubject(data);
        break;

      default:
        console.log(`Unhandled event type: ${event}`);
    }

    // Mark as processed
    if (eventId) {
      processedEvents.add(eventId);
      // Clean up old events periodically
      if (processedEvents.size > 10000) {
        const entries = Array.from(processedEvents);
        entries.slice(0, 5000).forEach(id => processedEvents.delete(id));
      }
    }

    res.status(200).json({ status: 'processed' });

  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function handleConsentUpdate(data) {
  const { dataSubjectId, dataControllerId, purposes } = data;
  console.log('Consent updated:', { dataSubjectId, purposes });

  // Update your systems based on consent changes
  for (const [purposeId, granted] of Object.entries(purposes)) {
    if (!granted) {
      // Consent revoked - trigger data processing changes
      await revokeDataProcessing(dataSubjectId, purposeId);
    }
  }
}

async function handleRightsRequest(data) {
  const { requestId, requestType, dataSubjectId } = data;
  console.log('Rights request received:', { requestId, requestType });

  switch (requestType) {
    case 'access':
      // Queue data export job
      await queueDataExport(dataSubjectId, requestId);
      break;

    case 'erasure':
      // Queue deletion job
      await queueDataDeletion(dataSubjectId, requestId);
      break;

    case 'portability':
      // Queue portable format export
      await queuePortableExport(dataSubjectId, requestId);
      break;
  }
}

async function handleNewDataSubject(data) {
  const { dataSubjectId, dataControllerId, did } = data;
  console.log('New data subject:', { dataSubjectId, did });

  // Sync to your CRM or user database
  await syncToUserDatabase(dataSubjectId, did);
}

app.listen(3000, () => {
  console.log('Webhook handler listening on port 3000');
});

Caching Strategy

Response Caching with TTL

JavaScript
class CachedDxtraClient extends DxtraClient {
  constructor(config, cacheTtlMs = 60000) {
    super(config);
    this.cache = new Map();
    this.cacheTtlMs = cacheTtlMs;
  }

  getCacheKey(query, variables) {
    return JSON.stringify({ query, variables });
  }

  async request(query, variables, options = {}) {
    const { skipCache = false, forceFresh = false } = options;

    if (!skipCache && !forceFresh) {
      const cacheKey = this.getCacheKey(query, variables);
      const cached = this.cache.get(cacheKey);

      if (cached && Date.now() < cached.expiresAt) {
        return cached.data;
      }
    }

    const data = await super.request(query, variables);

    if (!skipCache) {
      const cacheKey = this.getCacheKey(query, variables);
      this.cache.set(cacheKey, {
        data,
        expiresAt: Date.now() + this.cacheTtlMs
      });
    }

    return data;
  }

  invalidateCache(pattern) {
    if (!pattern) {
      this.cache.clear();
      return;
    }

    for (const key of this.cache.keys()) {
      if (key.includes(pattern)) {
        this.cache.delete(key);
      }
    }
  }
}

// Usage
const client = new CachedDxtraClient(
  { personalAccessToken: process.env.DXTRA_PAT },
  60000 // 1 minute cache TTL
);

// Cached query
const data1 = await client.request('{ dataControllers { id } }');

// Force fresh data
const data2 = await client.request(
  '{ dataControllers { id } }',
  {},
  { forceFresh: true }
);

// Invalidate cache after mutation
await client.request(mutationQuery, variables);
client.invalidateCache('dataControllers');

Real-Time Updates

GraphQL Subscriptions

Subscribe to real-time data changes using WebSocket.

JavaScript
import { createClient } from 'graphql-ws';
import WebSocket from 'ws';

async function subscribeToChanges(personalAccessToken, controllerId) {
  // Get JWT token
  const authResponse = await fetch('https://auth.dxtra.ai/v1/signin/pat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ personalAccessToken })
  });

  const { session } = await authResponse.json();

  // Create WebSocket client
  const client = createClient({
    url: 'wss://api.dxtra.ai/v1/graphql',
    webSocketImpl: WebSocket,
    connectionParams: {
      headers: {
        'Authorization': `Bearer ${session.accessToken}`,
        'X-Hasura-Role': 'user'
      }
    }
  });

  // Subscribe to rights requests
  const unsubscribe = client.subscribe(
    {
      query: `
        subscription RightsRequestUpdates($controllerId: uuid!) {
          dataSubjectRightsRequests(
            where: { dataSubject: { dataControllerId: { _eq: $controllerId } } }
            orderBy: { updatedAt: desc }
            limit: 1
          ) {
            id
            requestType
            status
            updatedAt
          }
        }
      `,
      variables: { controllerId }
    },
    {
      next: (result) => {
        console.log('Rights request update:', result.data);
      },
      error: (error) => {
        console.error('Subscription error:', error);
      },
      complete: () => {
        console.log('Subscription completed');
      }
    }
  );

  // Cleanup
  return unsubscribe;
}

Testing Patterns

Mock Client for Unit Tests

JavaScript
class MockDxtraClient {
  constructor(responses = {}) {
    this.responses = responses;
    this.calls = [];
  }

  async request(query, variables) {
    this.calls.push({ query, variables });

    // Find matching mock response
    for (const [pattern, response] of Object.entries(this.responses)) {
      if (query.includes(pattern)) {
        if (typeof response === 'function') {
          return response(query, variables);
        }
        return response;
      }
    }

    throw new Error(`No mock response for query: ${query.slice(0, 100)}...`);
  }

  getCallCount() {
    return this.calls.length;
  }

  getLastCall() {
    return this.calls[this.calls.length - 1];
  }

  reset() {
    this.calls = [];
  }
}

// Usage in tests
describe('Data Controller Service', () => {
  let mockClient;

  beforeEach(() => {
    mockClient = new MockDxtraClient({
      'dataControllers': {
        dataControllers: [
          { id: 'test-uuid', title: 'Test Controller' }
        ]
      },
      'dataSubjects': (query, variables) => ({
        dataSubjects: [
          { id: 'subject-1', dataControllerId: variables.controllerId }
        ]
      })
    });
  });

  test('fetches data controllers', async () => {
    const service = new DataControllerService(mockClient);
    const controllers = await service.getControllers();

    expect(controllers).toHaveLength(1);
    expect(controllers[0].title).toBe('Test Controller');
    expect(mockClient.getCallCount()).toBe(1);
  });
});

Performance Optimization

Query Complexity Management

JavaScript
// Avoid deeply nested queries
// Bad - causes N+1 queries
const badQuery = `
  query {
    dataControllers {
      id
      dataSubjects {
        id
        rightsRequests {
          id
          dataSubject {
            # Circular reference
            dataController {
              id
            }
          }
        }
      }
    }
  }
`;

// Good - flat structure with explicit relationships
const goodQuery = `
  query GetControllerData($controllerId: uuid!) {
    dataController(id: $controllerId) {
      id
      title
    }

    dataSubjects(
      where: { dataControllerId: { _eq: $controllerId } }
      limit: 100
    ) {
      id
      did
    }

    dataSubjectRightsRequests(
      where: { dataSubject: { dataControllerId: { _eq: $controllerId } } }
      limit: 50
    ) {
      id
      requestType
      dataSubjectId
    }
  }
`;

Request Batching

JavaScript
import DataLoader from 'dataloader';

// Batch data subject lookups
const dataSubjectLoader = new DataLoader(async (ids) => {
  const query = `
    query GetDataSubjects($ids: [uuid!]!) {
      dataSubjects(where: { id: { _in: $ids } }) {
        id
        did
        createdAt
      }
    }
  `;

  const result = await client.request(query, { ids });

  // Return in same order as requested
  return ids.map(id =>
    result.dataSubjects.find(s => s.id === id) || null
  );
});

// Usage - automatically batches multiple lookups
const [subject1, subject2, subject3] = await Promise.all([
  dataSubjectLoader.load('uuid-1'),
  dataSubjectLoader.load('uuid-2'),
  dataSubjectLoader.load('uuid-3')
]);

Next Steps