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¶
- API Authentication - Security best practices
- GraphQL Reference - Complete schema
- Error Handling - Error code reference
- Rate Limits - Usage optimization