Morten Sæther

← Blog

Episode 22: Custom API Development for Power Pages

1 June 2026

Building Robust External Integrations and Data Exchange Patterns

Introduction

Modern Power Pages portals rarely exist in isolation—they need to connect with external systems, third-party APIs, and legacy applications. This episode dives deep into custom API development patterns that enable seamless data exchange, real-time integrations, and enterprise-grade connectivity solutions.

Whether you’re integrating with CRM systems, external databases, or modern SaaS platforms, mastering API development patterns is essential for creating truly powerful portal experiences.

What You’ll Learn

In this advanced episode, you’ll master:

Prerequisites

Core API Development Patterns

REST API Integration Fundamentals

Power Pages provides multiple approaches for API integration, each suited to different scenarios:

Client-Side Integration (JavaScript)

// Modern fetch API with error handling
class PowerPagesAPIClient {
    constructor(baseURL, apiKey) {
        this.baseURL = baseURL;
        this.apiKey = apiKey;
        this.defaultHeaders = {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiKey}`
        };
    }

    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        const config = {
            headers: { ...this.defaultHeaders, ...options.headers },
            ...options
        };

        try {
            const response = await fetch(url, config);
            
            if (!response.ok) {
                throw new APIError(
                    `HTTP ${response.status}: ${response.statusText}`,
                    response.status
                );
            }

            return await response.json();
        } catch (error) {
            this.handleError(error);
            throw error;
        }
    }

    async get(endpoint, params = {}) {
        const queryString = new URLSearchParams(params).toString();
        const url = queryString ? `${endpoint}?${queryString}` : endpoint;
        return this.request(url, { method: 'GET' });
    }

    async post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }

    async put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }

    async delete(endpoint) {
        return this.request(endpoint, { method: 'DELETE' });
    }

    handleError(error) {
        console.error('API Error:', error);
        // Log to Power Pages or external monitoring system
        this.logError(error);
    }

    logError(error) {
        // Implementation depends on your logging strategy
        if (window.applicationInsights) {
            window.applicationInsights.trackException({ exception: error });
        }
    }
}

// Custom error class for API operations
class APIError extends Error {
    constructor(message, status) {
        super(message);
        this.name = 'APIError';
        this.status = status;
    }
}

Server-Side Integration (Power Automate)

{
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "actions": {
            "HTTP_Request": {
                "type": "Http",
                "inputs": {
                    "method": "GET",
                    "uri": "@parameters('externalApiUrl')",
                    "headers": {
                        "Authorization": "@concat('Bearer ', parameters('apiToken'))",
                        "Content-Type": "application/json"
                    }
                }
            },
            "Parse_JSON": {
                "type": "ParseJson",
                "inputs": {
                    "content": "@body('HTTP_Request')",
                    "schema": {
                        "type": "object",
                        "properties": {
                            "data": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "id": { "type": "string" },
                                        "name": { "type": "string" },
                                        "status": { "type": "string" }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "Transform_Data": {
                "type": "Select",
                "inputs": {
                    "from": "@body('Parse_JSON')?['data']",
                    "select": {
                        "entityId": "@item()?['id']",
                        "displayName": "@item()?['name']",
                        "currentStatus": "@item()?['status']",
                        "lastUpdated": "@utcNow()"
                    }
                }
            }
        }
    }
}

Advanced Authentication Patterns

OAuth 2.0 Implementation

// OAuth 2.0 authentication manager
class OAuthManager {
    constructor(clientId, redirectUri, authUrl, tokenUrl) {
        this.clientId = clientId;
        this.redirectUri = redirectUri;
        this.authUrl = authUrl;
        this.tokenUrl = tokenUrl;
        this.accessToken = localStorage.getItem('oauth_access_token');
        this.refreshToken = localStorage.getItem('oauth_refresh_token');
    }

    // Initiate OAuth flow
    authorize(scopes = []) {
        const params = new URLSearchParams({
            response_type: 'code',
            client_id: this.clientId,
            redirect_uri: this.redirectUri,
            scope: scopes.join(' '),
            state: this.generateState()
        });

        window.location.href = `${this.authUrl}?${params}`;
    }

    // Handle OAuth callback
    async handleCallback() {
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');
        const state = urlParams.get('state');

        if (!this.validateState(state)) {
            throw new Error('Invalid OAuth state parameter');
        }

        if (code) {
            await this.exchangeCodeForTokens(code);
        }
    }

    // Exchange authorization code for tokens
    async exchangeCodeForTokens(code) {
        const response = await fetch(this.tokenUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                client_id: this.clientId,
                code: code,
                redirect_uri: this.redirectUri
            })
        });

        const data = await response.json();
        
        this.accessToken = data.access_token;
        this.refreshToken = data.refresh_token;
        
        localStorage.setItem('oauth_access_token', this.accessToken);
        localStorage.setItem('oauth_refresh_token', this.refreshToken);
    }

    // Refresh access token
    async refreshAccessToken() {
        if (!this.refreshToken) {
            throw new Error('No refresh token available');
        }

        const response = await fetch(this.tokenUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                grant_type: 'refresh_token',
                refresh_token: this.refreshToken,
                client_id: this.clientId
            })
        });

        const data = await response.json();
        this.accessToken = data.access_token;
        localStorage.setItem('oauth_access_token', this.accessToken);
    }

    generateState() {
        const state = Math.random().toString(36).substring(2, 15);
        sessionStorage.setItem('oauth_state', state);
        return state;
    }

    validateState(state) {
        const storedState = sessionStorage.getItem('oauth_state');
        sessionStorage.removeItem('oauth_state');
        return state === storedState;
    }

    isAuthenticated() {
        return !!this.accessToken;
    }

    getAuthHeaders() {
        return {
            'Authorization': `Bearer ${this.accessToken}`
        };
    }
}

GraphQL Integration Patterns

GraphQL Client Implementation

// GraphQL client for Power Pages
class GraphQLClient {
    constructor(endpoint, authManager) {
        this.endpoint = endpoint;
        this.authManager = authManager;
    }

    async query(query, variables = {}) {
        return this.request({ query, variables });
    }

    async mutation(mutation, variables = {}) {
        return this.request({ query: mutation, variables });
    }

    async request(payload) {
        const headers = {
            'Content-Type': 'application/json',
            ...this.authManager.getAuthHeaders()
        };

        try {
            const response = await fetch(this.endpoint, {
                method: 'POST',
                headers,
                body: JSON.stringify(payload)
            });

            const result = await response.json();

            if (result.errors) {
                throw new GraphQLError(result.errors);
            }

            return result.data;
        } catch (error) {
            if (error.status === 401) {
                await this.authManager.refreshAccessToken();
                return this.request(payload); // Retry once
            }
            throw error;
        }
    }
}

// Example GraphQL queries and mutations
const USER_QUERIES = {
    GET_USER_PROFILE: `
        query GetUserProfile($userId: ID!) {
            user(id: $userId) {
                id
                name
                email
                profile {
                    avatar
                    department
                    lastLogin
                }
                permissions {
                    canEdit
                    canDelete
                    canApprove
                }
            }
        }
    `,
    
    UPDATE_USER_PROFILE: `
        mutation UpdateUserProfile($userId: ID!, $input: UserUpdateInput!) {
            updateUser(id: $userId, input: $input) {
                id
                name
                email
                updatedAt
            }
        }
    `,
    
    SEARCH_USERS: `
        query SearchUsers($searchTerm: String!, $filters: UserFilters) {
            searchUsers(term: $searchTerm, filters: $filters) {
                totalCount
                pageInfo {
                    hasNextPage
                    hasPreviousPage
                    startCursor
                    endCursor
                }
                edges {
                    node {
                        id
                        name
                        email
                        department
                    }
                }
            }
        }
    `
};

// Usage example in Power Pages
class UserManager {
    constructor(graphqlClient) {
        this.client = graphqlClient;
    }

    async getUserProfile(userId) {
        return this.client.query(USER_QUERIES.GET_USER_PROFILE, { userId });
    }

    async updateUserProfile(userId, updates) {
        return this.client.mutation(USER_QUERIES.UPDATE_USER_PROFILE, {
            userId,
            input: updates
        });
    }

    async searchUsers(searchTerm, filters = {}) {
        return this.client.query(USER_QUERIES.SEARCH_USERS, {
            searchTerm,
            filters
        });
    }
}

Data Transformation and Mapping

Advanced Data Transformation Patterns

// Data transformation engine for API responses
class DataTransformer {
    constructor() {
        this.transformations = new Map();
    }

    // Register transformation schemas
    registerTransformation(name, schema) {
        this.transformations.set(name, schema);
    }

    // Transform data using registered schema
    transform(data, schemaName, context = {}) {
        const schema = this.transformations.get(schemaName);
        if (!schema) {
            throw new Error(`Transformation schema '${schemaName}' not found`);
        }

        if (Array.isArray(data)) {
            return data.map(item => this.transformObject(item, schema, context));
        }

        return this.transformObject(data, schema, context);
    }

    transformObject(obj, schema, context) {
        const result = {};

        for (const [targetKey, mapping] of Object.entries(schema)) {
            if (typeof mapping === 'string') {
                // Simple field mapping
                result[targetKey] = this.getNestedValue(obj, mapping);
            } else if (typeof mapping === 'function') {
                // Function transformation
                result[targetKey] = mapping(obj, context);
            } else if (mapping.type === 'computed') {
                // Computed field
                result[targetKey] = this.computeValue(obj, mapping, context);
            } else if (mapping.type === 'nested') {
                // Nested transformation
                const nestedData = this.getNestedValue(obj, mapping.source);
                result[targetKey] = this.transform(nestedData, mapping.schema, context);
            }
        }

        return result;
    }

    getNestedValue(obj, path) {
        return path.split('.').reduce((current, key) => current?.[key], obj);
    }

    computeValue(obj, mapping, context) {
        const dependencies = mapping.dependencies || [];
        const values = dependencies.map(dep => this.getNestedValue(obj, dep));
        return mapping.compute(...values, context);
    }
}

// Example transformation schemas
const TRANSFORMATION_SCHEMAS = {
    // Transform external API user to Power Pages contact
    EXTERNAL_USER_TO_CONTACT: {
        contactId: 'id',
        fullName: (obj) => `${obj.firstName} ${obj.lastName}`,
        emailAddress: 'email',
        phoneNumber: 'phone',
        department: 'attributes.department',
        isActive: (obj) => obj.status === 'active',
        lastLoginDate: {
            type: 'computed',
            dependencies: ['lastLogin'],
            compute: (lastLogin) => lastLogin ? new Date(lastLogin).toISOString() : null
        },
        address: {
            type: 'nested',
            source: 'address',
            schema: 'ADDRESS_MAPPING'
        }
    },

    ADDRESS_MAPPING: {
        street: 'street1',
        city: 'city',
        state: 'state',
        postalCode: 'zipCode',
        country: 'country'
    },

    // Transform Power Pages form data to external API format
    CONTACT_FORM_TO_EXTERNAL: {
        firstName: 'firstname',
        lastName: 'lastname',
        email: 'emailaddress1',
        phone: 'telephone1',
        company: 'parentcustomerid.name',
        jobTitle: 'jobtitle',
        customFields: {
            type: 'computed',
            dependencies: ['*'],
            compute: (obj, context) => {
                const customFields = {};
                for (const [key, value] of Object.entries(obj)) {
                    if (key.startsWith('custom_')) {
                        customFields[key.replace('custom_', '')] = value;
                    }
                }
                return customFields;
            }
        }
    }
};

// Initialize transformer with schemas
const transformer = new DataTransformer();
Object.entries(TRANSFORMATION_SCHEMAS).forEach(([name, schema]) => {
    transformer.registerTransformation(name, schema);
});

Caching and Performance Optimization

Advanced Caching Strategies

// Multi-level caching system for API responses
class APICache {
    constructor(options = {}) {
        this.memoryCache = new Map();
        this.sessionCache = window.sessionStorage;
        this.localCache = window.localStorage;
        this.defaultTTL = options.defaultTTL || 300000; // 5 minutes
        this.maxMemorySize = options.maxMemorySize || 100;
    }

    // Generate cache key from request parameters
    generateKey(url, params = {}) {
        const paramString = JSON.stringify(params, Object.keys(params).sort());
        return `api_cache_${btoa(url + paramString)}`;
    }

    // Store data in appropriate cache level
    set(key, data, ttl = this.defaultTTL, level = 'memory') {
        const cacheItem = {
            data,
            timestamp: Date.now(),
            ttl,
            level
        };

        switch (level) {
            case 'memory':
                this.setMemoryCache(key, cacheItem);
                break;
            case 'session':
                this.sessionCache.setItem(key, JSON.stringify(cacheItem));
                break;
            case 'local':
                this.localCache.setItem(key, JSON.stringify(cacheItem));
                break;
        }
    }

    setMemoryCache(key, item) {
        if (this.memoryCache.size >= this.maxMemorySize) {
            // Remove oldest item
            const firstKey = this.memoryCache.keys().next().value;
            this.memoryCache.delete(firstKey);
        }
        this.memoryCache.set(key, item);
    }

    // Retrieve data from cache
    get(key) {
        // Check memory cache first
        let item = this.memoryCache.get(key);
        if (item && this.isValid(item)) {
            return item.data;
        }

        // Check session cache
        const sessionItem = this.sessionCache.getItem(key);
        if (sessionItem) {
            item = JSON.parse(sessionItem);
            if (this.isValid(item)) {
                // Promote to memory cache
                this.memoryCache.set(key, item);
                return item.data;
            }
            this.sessionCache.removeItem(key);
        }

        // Check local cache
        const localItem = this.localCache.getItem(key);
        if (localItem) {
            item = JSON.parse(localItem);
            if (this.isValid(item)) {
                // Promote to session cache
                this.sessionCache.setItem(key, JSON.stringify(item));
                return item.data;
            }
            this.localCache.removeItem(key);
        }

        return null;
    }

    isValid(item) {
        return Date.now() - item.timestamp < item.ttl;
    }

    // Clear expired items
    cleanup() {
        // Memory cache cleanup
        for (const [key, item] of this.memoryCache.entries()) {
            if (!this.isValid(item)) {
                this.memoryCache.delete(key);
            }
        }

        // Session cache cleanup
        this.cleanupStorage(this.sessionCache);
        
        // Local cache cleanup
        this.cleanupStorage(this.localCache);
    }

    cleanupStorage(storage) {
        const keysToRemove = [];
        for (let i = 0; i < storage.length; i++) {
            const key = storage.key(i);
            if (key?.startsWith('api_cache_')) {
                try {
                    const item = JSON.parse(storage.getItem(key));
                    if (!this.isValid(item)) {
                        keysToRemove.push(key);
                    }
                } catch (e) {
                    keysToRemove.push(key);
                }
            }
        }
        keysToRemove.forEach(key => storage.removeItem(key));
    }

    // Cache-aware API request wrapper
    async cachedRequest(apiClient, method, endpoint, params = {}, options = {}) {
        const cacheKey = this.generateKey(`${method}:${endpoint}`, params);
        
        // Try to get from cache first
        const cachedData = this.get(cacheKey);
        if (cachedData) {
            return cachedData;
        }

        // Make API request
        const data = await apiClient[method](endpoint, params);
        
        // Cache the response
        const ttl = options.ttl || this.defaultTTL;
        const level = options.cacheLevel || 'memory';
        this.set(cacheKey, data, ttl, level);

        return data;
    }
}

// Usage with API client
class CachedAPIClient extends PowerPagesAPIClient {
    constructor(baseURL, apiKey, cacheOptions = {}) {
        super(baseURL, apiKey);
        this.cache = new APICache(cacheOptions);
        
        // Clean up cache periodically
        setInterval(() => this.cache.cleanup(), 60000); // Every minute
    }

    async get(endpoint, params = {}, cacheOptions = {}) {
        if (cacheOptions.useCache !== false) {
            return this.cache.cachedRequest(this, 'get', endpoint, params, cacheOptions);
        }
        return super.get(endpoint, params);
    }
}

Error Handling and Resilience Patterns

Circuit Breaker Pattern for API Calls

// Circuit breaker implementation for external API calls
class CircuitBreaker {
    constructor(options = {}) {
        this.failureThreshold = options.failureThreshold || 5;
        this.resetTimeout = options.resetTimeout || 60000;
        this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
        this.failureCount = 0;
        this.lastFailureTime = null;
        this.successCount = 0;
    }

    async execute(apiCall) {
        if (this.state === 'OPEN') {
            if (Date.now() - this.lastFailureTime > this.resetTimeout) {
                this.state = 'HALF_OPEN';
                this.successCount = 0;
            } else {
                throw new Error('Circuit breaker is OPEN');
            }
        }

        try {
            const result = await apiCall();
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure();
            throw error;
        }
    }

    onSuccess() {
        this.failureCount = 0;
        if (this.state === 'HALF_OPEN') {
            this.successCount++;
            if (this.successCount >= 3) {
                this.state = 'CLOSED';
            }
        }
    }

    onFailure() {
        this.failureCount++;
        this.lastFailureTime = Date.now();
        if (this.failureCount >= this.failureThreshold) {
            this.state = 'OPEN';
        }
    }

    getState() {
        return {
            state: this.state,
            failureCount: this.failureCount,
            lastFailureTime: this.lastFailureTime
        };
    }
}

// Retry pattern with exponential backoff
class RetryHandler {
    constructor(options = {}) {
        this.maxRetries = options.maxRetries || 3;
        this.baseDelay = options.baseDelay || 1000;
        this.maxDelay = options.maxDelay || 10000;
        this.backoffFactor = options.backoffFactor || 2;
    }

    async execute(apiCall, retryCondition = this.defaultRetryCondition) {
        let lastError;
        
        for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
            try {
                return await apiCall();
            } catch (error) {
                lastError = error;
                
                if (attempt === this.maxRetries || !retryCondition(error, attempt)) {
                    throw error;
                }

                const delay = this.calculateDelay(attempt);
                await this.sleep(delay);
            }
        }
        
        throw lastError;
    }

    calculateDelay(attempt) {
        const delay = this.baseDelay * Math.pow(this.backoffFactor, attempt);
        return Math.min(delay, this.maxDelay);
    }

    defaultRetryCondition(error, attempt) {
        // Retry on network errors or 5xx server errors
        return error.status >= 500 || error.name === 'NetworkError';
    }

    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

// Resilient API client combining circuit breaker and retry
class ResilientAPIClient extends CachedAPIClient {
    constructor(baseURL, apiKey, options = {}) {
        super(baseURL, apiKey, options.cacheOptions);
        this.circuitBreaker = new CircuitBreaker(options.circuitBreakerOptions);
        this.retryHandler = new RetryHandler(options.retryOptions);
    }

    async request(endpoint, options = {}) {
        const apiCall = () => super.request(endpoint, options);
        
        if (options.useCircuitBreaker !== false) {
            return this.circuitBreaker.execute(() => 
                this.retryHandler.execute(apiCall)
            );
        }
        
        return this.retryHandler.execute(apiCall);
    }
}

Rate Limiting and Throttling

Advanced Rate Limiting Implementation

// Token bucket rate limiter
class TokenBucket {
    constructor(capacity, refillRate, refillPeriod = 1000) {
        this.capacity = capacity;
        this.tokens = capacity;
        this.refillRate = refillRate;
        this.refillPeriod = refillPeriod;
        this.lastRefill = Date.now();
    }

    consume(tokens = 1) {
        this.refill();
        
        if (this.tokens >= tokens) {
            this.tokens -= tokens;
            return true;
        }
        
        return false;
    }

    refill() {
        const now = Date.now();
        const timePassed = now - this.lastRefill;
        const tokensToAdd = Math.floor((timePassed / this.refillPeriod) * this.refillRate);
        
        this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
        this.lastRefill = now;
    }

    getStatus() {
        this.refill();
        return {
            tokens: this.tokens,
            capacity: this.capacity,
            percentage: (this.tokens / this.capacity) * 100
        };
    }
}

// Rate-limited API client
class RateLimitedAPIClient extends ResilientAPIClient {
    constructor(baseURL, apiKey, options = {}) {
        super(baseURL, apiKey, options);
        this.rateLimiter = new TokenBucket(
            options.rateLimit?.capacity || 100,
            options.rateLimit?.refillRate || 10,
            options.rateLimit?.refillPeriod || 1000
        );
        this.rateLimitQueue = [];
        this.isProcessingQueue = false;
    }

    async request(endpoint, options = {}) {
        return new Promise((resolve, reject) => {
            this.rateLimitQueue.push({
                endpoint,
                options,
                resolve,
                reject,
                timestamp: Date.now()
            });

            this.processQueue();
        });
    }

    async processQueue() {
        if (this.isProcessingQueue || this.rateLimitQueue.length === 0) {
            return;
        }

        this.isProcessingQueue = true;

        while (this.rateLimitQueue.length > 0) {
            if (!this.rateLimiter.consume()) {
                // Wait for tokens to be available
                await this.sleep(100);
                continue;
            }

            const request = this.rateLimitQueue.shift();
            
            try {
                const result = await super.request(request.endpoint, request.options);
                request.resolve(result);
            } catch (error) {
                request.reject(error);
            }
        }

        this.isProcessingQueue = false;
    }

    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    getRateLimitStatus() {
        return this.rateLimiter.getStatus();
    }
}

Real-World Integration Examples

CRM Integration Pattern

Salesforce Integration Example

// Salesforce API integration for Power Pages
class SalesforceIntegration {
    constructor(instanceUrl, accessToken) {
        this.apiClient = new RateLimitedAPIClient(
            `${instanceUrl}/services/data/v58.0`,
            accessToken,
            {
                rateLimit: { capacity: 100, refillRate: 20 },
                circuitBreakerOptions: { failureThreshold: 3 }
            }
        );
    }

    // Get account information
    async getAccount(accountId) {
        const endpoint = `/sobjects/Account/${accountId}`;
        return this.apiClient.get(endpoint, {}, { 
            useCache: true, 
            ttl: 300000, // 5 minutes
            cacheLevel: 'session'
        });
    }

    // Search for contacts
    async searchContacts(searchTerm, filters = {}) {
        const soql = this.buildContactSearchQuery(searchTerm, filters);
        const endpoint = `/query/?q=${encodeURIComponent(soql)}`;
        return this.apiClient.get(endpoint);
    }

    buildContactSearchQuery(searchTerm, filters) {
        let query = `SELECT Id, FirstName, LastName, Email, Phone, AccountId 
                     FROM Contact 
                     WHERE (Name LIKE '%${searchTerm}%' OR Email LIKE '%${searchTerm}%')`;
        
        if (filters.accountId) {
            query += ` AND AccountId = '${filters.accountId}'`;
        }
        
        if (filters.isActive) {
            query += ` AND IsActive = true`;
        }
        
        query += ` ORDER BY LastModifiedDate DESC LIMIT 50`;
        return query;
    }

    // Create or update contact
    async upsertContact(contactData) {
        const externalId = contactData.email;
        const endpoint = `/sobjects/Contact/Email__c/${externalId}`;
        
        const salesforceData = this.transformToSalesforceFormat(contactData);
        
        try {
            return await this.apiClient.request(endpoint, {
                method: 'PATCH',
                body: JSON.stringify(salesforceData)
            });
        } catch (error) {
            if (error.status === 404) {
                // Contact doesn't exist, create new one
                return this.createContact(salesforceData);
            }
            throw error;
        }
    }

    async createContact(contactData) {
        const endpoint = '/sobjects/Contact/';
        return this.apiClient.post(endpoint, contactData);
    }

    transformToSalesforceFormat(powerPagesData) {
        return {
            FirstName: powerPagesData.firstname,
            LastName: powerPagesData.lastname,
            Email: powerPagesData.emailaddress1,
            Phone: powerPagesData.telephone1,
            Title: powerPagesData.jobtitle,
            Description: powerPagesData.description,
            // Map custom fields
            Custom_Field_1__c: powerPagesData.custom_field_1,
            Custom_Field_2__c: powerPagesData.custom_field_2
        };
    }
}

E-commerce Platform Integration

Shopify Integration Example

// Shopify API integration for order management
class ShopifyIntegration {
    constructor(shopName, accessToken) {
        this.apiClient = new RateLimitedAPIClient(
            `https://${shopName}.myshopify.com/admin/api/2023-10`,
            accessToken,
            {
                rateLimit: { capacity: 40, refillRate: 2 }, // Shopify rate limit
                cacheOptions: { defaultTTL: 60000 } // 1 minute for order data
            }
        );
    }

    // Get customer orders
    async getCustomerOrders(customerId, limit = 50) {
        const endpoint = `/customers/${customerId}/orders.json`;
        const params = { 
            limit, 
            status: 'any',
            fields: 'id,order_number,created_at,total_price,financial_status,fulfillment_status,line_items'
        };
        
        const response = await this.apiClient.get(endpoint, params, {
            useCache: true,
            ttl: 300000, // 5 minutes
            cacheLevel: 'session'
        });
        
        return response.orders.map(order => this.transformOrder(order));
    }

    // Get order details
    async getOrderDetails(orderId) {
        const endpoint = `/orders/${orderId}.json`;
        const response = await this.apiClient.get(endpoint, {}, {
            useCache: true,
            ttl: 60000 // 1 minute for fresh order status
        });
        
        return this.transformOrderDetails(response.order);
    }

    transformOrder(shopifyOrder) {
        return {
            orderId: shopifyOrder.id,
            orderNumber: shopifyOrder.order_number,
            orderDate: shopifyOrder.created_at,
            totalAmount: parseFloat(shopifyOrder.total_price),
            status: this.mapOrderStatus(shopifyOrder.financial_status, shopifyOrder.fulfillment_status),
            itemCount: shopifyOrder.line_items?.length || 0,
            trackingAvailable: shopifyOrder.fulfillment_status === 'fulfilled'
        };
    }

    transformOrderDetails(shopifyOrder) {
        return {
            ...this.transformOrder(shopifyOrder),
            items: shopifyOrder.line_items.map(item => ({
                productId: item.product_id,
                variantId: item.variant_id,
                name: item.name,
                quantity: item.quantity,
                price: parseFloat(item.price),
                sku: item.sku
            })),
            shippingAddress: shopifyOrder.shipping_address,
            billingAddress: shopifyOrder.billing_address,
            fulfillments: shopifyOrder.fulfillments?.map(f => ({
                trackingNumber: f.tracking_number,
                trackingUrl: f.tracking_url,
                status: f.status
            })) || []
        };
    }

    mapOrderStatus(financialStatus, fulfillmentStatus) {
        if (financialStatus === 'pending') return 'Payment Pending';
        if (financialStatus === 'paid' && !fulfillmentStatus) return 'Processing';
        if (fulfillmentStatus === 'fulfilled') return 'Shipped';
        if (fulfillmentStatus === 'partial') return 'Partially Shipped';
        return 'Unknown';
    }
}

Testing and Debugging API Integrations

Comprehensive Testing Strategy

API Integration Test Suite

// Test utilities for API integrations
class APITestSuite {
    constructor(apiClient) {
        this.apiClient = apiClient;
        this.testResults = [];
    }

    async runAllTests() {
        console.log('Starting API integration tests...');
        
        const tests = [
            { name: 'Authentication', test: () => this.testAuthentication() },
            { name: 'Basic CRUD Operations', test: () => this.testCrudOperations() },
            { name: 'Error Handling', test: () => this.testErrorHandling() },
            { name: 'Rate Limiting', test: () => this.testRateLimiting() },
            { name: 'Data Transformation', test: () => this.testDataTransformation() },
            { name: 'Caching', test: () => this.testCaching() }
        ];

        for (const testCase of tests) {
            try {
                console.log(`Running test: ${testCase.name}`);
                const startTime = Date.now();
                await testCase.test();
                const duration = Date.now() - startTime;
                
                this.testResults.push({
                    name: testCase.name,
                    status: 'PASSED',
                    duration
                });
                console.log(`✅ ${testCase.name} - PASSED (${duration}ms)`);
            } catch (error) {
                this.testResults.push({
                    name: testCase.name,
                    status: 'FAILED',
                    error: error.message
                });
                console.error(`❌ ${testCase.name} - FAILED: ${error.message}`);
            }
        }

        this.printTestSummary();
        return this.testResults;
    }

    async testAuthentication() {
        if (!this.apiClient.isAuthenticated()) {
            throw new Error('API client is not authenticated');
        }
        
        // Test a simple authenticated request
        await this.apiClient.get('/test/auth');
    }

    async testCrudOperations() {
        // Create
        const createData = { name: 'Test Item', description: 'Test Description' };
        const created = await this.apiClient.post('/test/items', createData);
        
        if (!created.id) {
            throw new Error('Create operation failed - no ID returned');
        }

        // Read
        const retrieved = await this.apiClient.get(`/test/items/${created.id}`);
        if (retrieved.name !== createData.name) {
            throw new Error('Read operation failed - data mismatch');
        }

        // Update
        const updateData = { name: 'Updated Test Item' };
        const updated = await this.apiClient.put(`/test/items/${created.id}`, updateData);
        if (updated.name !== updateData.name) {
            throw new Error('Update operation failed');
        }

        // Delete
        await this.apiClient.delete(`/test/items/${created.id}`);
        
        // Verify deletion
        try {
            await this.apiClient.get(`/test/items/${created.id}`);
            throw new Error('Delete operation failed - item still exists');
        } catch (error) {
            if (error.status !== 404) {
                throw new Error('Delete operation failed - unexpected error');
            }
        }
    }

    async testErrorHandling() {
        // Test 404 error
        try {
            await this.apiClient.get('/test/nonexistent');
            throw new Error('Expected 404 error was not thrown');
        } catch (error) {
            if (error.status !== 404) {
                throw new Error(`Expected 404, got ${error.status}`);
            }
        }

        // Test invalid data error
        try {
            await this.apiClient.post('/test/items', { invalid: 'data' });
            throw new Error('Expected validation error was not thrown');
        } catch (error) {
            if (error.status !== 400) {
                throw new Error(`Expected 400, got ${error.status}`);
            }
        }
    }

    async testRateLimiting() {
        if (!this.apiClient.getRateLimitStatus) {
            console.log('Rate limiting not implemented - skipping test');
            return;
        }

        const initialStatus = this.apiClient.getRateLimitStatus();
        const initialTokens = initialStatus.tokens;

        // Make several requests
        for (let i = 0; i < 5; i++) {
            await this.apiClient.get('/test/rate-limit');
        }

        const finalStatus = this.apiClient.getRateLimitStatus();
        if (finalStatus.tokens >= initialTokens) {
            throw new Error('Rate limiting not working - tokens not consumed');
        }
    }

    async testDataTransformation() {
        const testData = {
            externalId: '123',
            firstName: 'John',
            lastName: 'Doe',
            email: 'john.doe@example.com'
        };

        const transformer = new DataTransformer();
        // Assume transformation is registered
        const transformed = transformer.transform(testData, 'TEST_TRANSFORMATION');
        
        if (!transformed.fullName || transformed.fullName !== 'John Doe') {
            throw new Error('Data transformation failed');
        }
    }

    async testCaching() {
        if (!this.apiClient.cache) {
            console.log('Caching not implemented - skipping test');
            return;
        }

        // First request (should hit API)
        const startTime1 = Date.now();
        await this.apiClient.get('/test/cache', {}, { useCache: true });
        const duration1 = Date.now() - startTime1;

        // Second request (should hit cache)
        const startTime2 = Date.now();
        await this.apiClient.get('/test/cache', {}, { useCache: true });
        const duration2 = Date.now() - startTime2;

        if (duration2 >= duration1) {
            throw new Error('Caching not working - second request took too long');
        }
    }

    printTestSummary() {
        const passed = this.testResults.filter(r => r.status === 'PASSED').length;
        const failed = this.testResults.filter(r => r.status === 'FAILED').length;
        
        console.log('\n=== Test Summary ===');
        console.log(`Total Tests: ${this.testResults.length}`);
        console.log(`Passed: ${passed}`);
        console.log(`Failed: ${failed}`);
        
        if (failed > 0) {
            console.log('\nFailed Tests:');
            this.testResults
                .filter(r => r.status === 'FAILED')
                .forEach(r => console.log(`- ${r.name}: ${r.error}`));
        }
    }
}

Performance Monitoring and Analytics

API Performance Monitoring

// Performance monitoring for API integrations
class APIPerformanceMonitor {
    constructor(options = {}) {
        this.metrics = new Map();
        this.alertThresholds = options.alertThresholds || {
            responseTime: 5000, // 5 seconds
            errorRate: 0.1, // 10%
            availability: 0.95 // 95%
        };
        this.monitoringInterval = options.monitoringInterval || 60000; // 1 minute
        this.startMonitoring();
    }

    // Record API call metrics
    recordAPICall(endpoint, method, duration, success, error = null) {
        const key = `${method}:${endpoint}`;
        
        if (!this.metrics.has(key)) {
            this.metrics.set(key, {
                totalCalls: 0,
                successfulCalls: 0,
                totalDuration: 0,
                errors: [],
                lastCall: null
            });
        }

        const metric = this.metrics.get(key);
        metric.totalCalls++;
        metric.totalDuration += duration;
        metric.lastCall = Date.now();

        if (success) {
            metric.successfulCalls++;
        } else {
            metric.errors.push({
                timestamp: Date.now(),
                error: error?.message || 'Unknown error',
                duration
            });
        }

        // Check for alerts
        this.checkAlerts(key, metric);
    }

    checkAlerts(endpoint, metric) {
        const avgResponseTime = metric.totalDuration / metric.totalCalls;
        const errorRate = (metric.totalCalls - metric.successfulCalls) / metric.totalCalls;

        if (avgResponseTime > this.alertThresholds.responseTime) {
            this.triggerAlert('HIGH_RESPONSE_TIME', endpoint, {
                avgResponseTime,
                threshold: this.alertThresholds.responseTime
            });
        }

        if (errorRate > this.alertThresholds.errorRate) {
            this.triggerAlert('HIGH_ERROR_RATE', endpoint, {
                errorRate,
                threshold: this.alertThresholds.errorRate
            });
        }
    }

    triggerAlert(type, endpoint, data) {
        const alert = {
            type,
            endpoint,
            timestamp: Date.now(),
            data
        };

        console.warn('API Performance Alert:', alert);
        
        // Send to monitoring service
        if (window.applicationInsights) {
            window.applicationInsights.trackEvent('APIPerformanceAlert', alert);
        }
    }

    getMetricsSummary() {
        const summary = {};
        
        for (const [endpoint, metric] of this.metrics.entries()) {
            summary[endpoint] = {
                totalCalls: metric.totalCalls,
                successRate: metric.successfulCalls / metric.totalCalls,
                avgResponseTime: metric.totalDuration / metric.totalCalls,
                recentErrors: metric.errors.slice(-10), // Last 10 errors
                lastCall: metric.lastCall
            };
        }

        return summary;
    }

    startMonitoring() {
        setInterval(() => {
            this.cleanupOldMetrics();
            this.generatePerformanceReport();
        }, this.monitoringInterval);
    }

    cleanupOldMetrics() {
        const oneHourAgo = Date.now() - (60 * 60 * 1000);
        
        for (const [endpoint, metric] of this.metrics.entries()) {
            // Remove old errors
            metric.errors = metric.errors.filter(
                error => error.timestamp > oneHourAgo
            );
        }
    }

    generatePerformanceReport() {
        const summary = this.getMetricsSummary();
        
        // Log performance metrics
        console.log('API Performance Report:', summary);
        
        // Send to analytics
        if (window.applicationInsights) {
            window.applicationInsights.trackEvent('APIPerformanceReport', summary);
        }
    }
}

// Integration with API clients
class MonitoredAPIClient extends RateLimitedAPIClient {
    constructor(baseURL, apiKey, options = {}) {
        super(baseURL, apiKey, options);
        this.performanceMonitor = new APIPerformanceMonitor(options.monitoring);
    }

    async request(endpoint, options = {}) {
        const startTime = Date.now();
        const method = options.method || 'GET';
        
        try {
            const result = await super.request(endpoint, options);
            const duration = Date.now() - startTime;
            
            this.performanceMonitor.recordAPICall(endpoint, method, duration, true);
            return result;
        } catch (error) {
            const duration = Date.now() - startTime;
            
            this.performanceMonitor.recordAPICall(endpoint, method, duration, false, error);
            throw error;
        }
    }

    getPerformanceMetrics() {
        return this.performanceMonitor.getMetricsSummary();
    }
}

Best Practices and Production Considerations

Security Best Practices

  1. API Key Management

    • Store API keys securely (Azure Key Vault, environment variables)
    • Rotate keys regularly
    • Use least-privilege access principles
    • Monitor API key usage
  2. Data Validation

    • Validate all input data before sending to APIs
    • Sanitize data to prevent injection attacks
    • Implement proper error handling for validation failures
  3. Transport Security

    • Always use HTTPS for API communications
    • Implement certificate pinning where appropriate
    • Use modern TLS versions (1.2+)

Performance Optimization

  1. Caching Strategy

    • Implement multi-level caching (memory, session, local)
    • Use appropriate cache TTL values
    • Implement cache invalidation strategies
  2. Request Optimization

    • Batch requests where possible
    • Use pagination for large datasets
    • Implement request deduplication
  3. Resource Management

    • Implement connection pooling
    • Use keep-alive connections
    • Monitor and limit concurrent requests

Monitoring and Observability

  1. Logging

    • Log all API requests and responses
    • Include correlation IDs for tracing
    • Implement structured logging
  2. Metrics

    • Track response times, error rates, throughput
    • Monitor API availability and uptime
    • Set up alerting for threshold breaches
  3. Tracing

    • Implement distributed tracing for complex integrations
    • Use Application Performance Monitoring (APM) tools
    • Track user journeys across systems

Summary

Custom API development in Power Pages requires a comprehensive approach that balances functionality, performance, security, and maintainability. By implementing the patterns and practices outlined in this episode, you’ll be able to create robust integrations that scale with your business needs.

Key Takeaways:

In our next episode, we’ll explore third-party integrations and enterprise system connectivity, building on the API development foundation you’ve learned here.


Resources and Code Repository

All code samples from this episode are available in the demo projects folder.

What’s Coming Next

Episode 23: Third-Party Integrations - We’ll dive into connecting Power Pages with CRM systems, external services, webhooks, and enterprise applications.


Ready to build robust API integrations? Start with the API client patterns and gradually add complexity as your requirements grow.