Qiaoqiao Zhang introduces the new modularized Azure SDK libraries for JavaScript, focusing on bundle size optimization, usability improvements, and architectural enhancements that benefit developers building Azure-integrated applications.

Azure SDK Modularized Libraries for JavaScript

By Qiaoqiao Zhang

Overview

Previously, the Azure SDK team introduced Azure REST libraries for JavaScript, which optimize browser user experience and minimize bundle size. While REST-level clients (RLCs) provide a direct abstraction with reduced code footprint, they aren’t always user-friendly for those more accustomed to traditional client libraries. This, combined with the challenge of reducing bundle size in browser apps, led to the development of modularized Azure SDK libraries.

Modularized Design

To bridge the gap between usability and bundle size, the Azure SDK team is creating new modularized libraries built atop RLCs. These libraries modularize API calls, promoting consistency and flexibility while reducing bundle size—benefiting both new and traditional client users.

Subpath Exports

Using subpath exports (since Node.js v12.7), the libraries enable tailored imports, comprising:

  • Modular API Layer: Each API call is modularized, letting customers import only what they need. It manages serialization for requests and deserialization for responses at the REST layer.
  • Service Client Layer: Offers the same experience as traditional clients, providing convenience layers built atop the API layer.

Sub Clients

In cases with multiple parallel or hierarchical sub clients, each sub client’s subpath export includes only the relevant APIs. Users can thus integrate only selected components, optimizing resource use and customizability.

Experimental Features

Experimental APIs can be previewed within dedicated subpath exports, giving early feedback and clearly delineating stable versus experimental APIs.

Opt-in Helpers

Core library dependencies are minimized. Instead, model serialization and deserialization logic moves to the client side as opt-in helpers, making advanced features (e.g., paging, long-running operations) optional and eliminating the resource load for unused code. This supports tree-shaking and lighter bundles.

ECMAScript Modules (ESM) vs. CommonJS (CJS)

Traditional clients were limited to CommonJS modules. The new libraries leverage tshy to support both ESM and CJS, letting developers choose the most compatible format.

Bundle Size Optimization

Different library layers demonstrate significant bundle size improvements:

Client Type Bundle Size Optimized Percentage
Traditional Client 124.64 KB N/A
Service Client Layer 91.39 KB 26.68%
Modular Layer 67.97 KB 45.47%
RLC 48.23 KB 61.30%

Conclusion: The modular layer provides meaningful improvements over traditional clients, balancing user experience and bundle size.

User Experience in Modularized Libraries

As an example, the modularized OpenAI library demonstrates usage patterns for each layer:

  • Authentication (with AzureKeyCredential):

    import { AzureKeyCredential } from "@azure/core-auth"
    const credential = new AzureKeyCredential(azureApiKey);
    
  • Endpoint & API Key Configuration:

    const endpoint = process.env['ENDPOINT'] || '';
    const azureApiKey = process.env['AZURE_API_KEY'] || '';
    
  • Model Selection:

    const deploymentName = 'gpt-35-turbo-1106';
    
  • Request Setup:

    const messages = [{ role: 'user', content: 'What is the weather like in Boston?' }];
    const tools = [
      {
        type: 'function',
        function: {
          name: 'get_current_weather',
          description: 'Get the current weather in a given location',
          parameters: {
            type: 'object',
            properties: {
              location: { type: 'string', description: 'The city and state, e.g. San Francisco, CA' },
              unit: { type: 'string', enum: ['celsius', 'fahrenheit'] },
            },
            required: ['location'],
          },
        },
      },
    ];
    

REST-Level Client (RLC) Usage

import createOpenAIContext from '@azure-rest/openai';
const client = createOpenAIContext(endpoint, credential);
const result = await client
  .path('/deployments/{deploymentId}/chat/completions', deploymentName)
  .post({ body: { messages, tools } });

// Handle unexpected results
if (isUnexpected(result)) {
  throw createRestError(result.body);
}

Modular API Layer Usage

import { getChatCompletions, createOpenAIContext } from "@azure/openai/api";
const context = createOpenAIContext(endpoint, credential);
const result = await getChatCompletions(context, deploymentName, messages, { tools });

Service Client Layer Usage

import { OpenAIClient } from "@azure/openai";
const client = new OpenAIClient(endpoint, credential);
const result = await client.getChatCompletions(deploymentName, messages, { tools });

Key Features for Modularized Libraries

Support for Complex Hierarchies

  • Parallel Sub Clients:

    import { LoadTestRunClient } from "@azure/loadtesting/loadTestRun";
    const loadTestRunClient = new LoadTestRunClient();
    loadTestRunClient.getTestRun();
    
    import { TestProfileAdministrationClient } from "@azure/loadtesting/testProfileAdministration";
    const profileAdminClient = new TestProfileAdministrationClient();
    profileAdminClient.getTestProfile();
    
  • Hierarchical Sub Clients:

    import { StorageClient } from "@azure/storage";
    const storageClient = new StorageClient(accountName);
    const storageContainerClient = storageClient.getContainerClient(containerId);
    storageContainerClient.upload();
      
    // Or import from subpath:
    import { StorageContainerClient } from "@azure/storage/container";
    const storageContainerClient = new StorageContainerClient(accountName, containerId);
    storageContainerClient.upload();
    
  • Model Namespace Hierarchies:

    import { ChatRequestMessage, ChatResponseMessage } from "@azure/openai/models/chat";
    import { CompletionRequest, CompletionResponse } from "@azure/openai/models/chat/completion";
    import { EmbeddingRequest, EmbeddingResponse } from "@azure/openai/models/chat/embedding";
    

Serialization and Deserialization

Serialization and deserialization logic is decentralized per model, improving bundle optimization and enabling tree-shaking. For instance:

function windowSerializer(obj: Window): any {
  return { width: obj.width, length: obj.length };
}

export function extensionSerializer(item: Extension): any {
  return {
    extension: !item["extension"] ? item["extension"] : extensionArraySerializer(item["extension"]),
    level: item["level"],
  };
}

export function extensionArraySerializer(result: Array<Extension>): any[] {
  return result.map(extensionSerializer);
}

export function elementSerializer(item: Element): any {
  return {
    extension: !item["extension"] ? item["extension"] : extensionArraySerializer(item["extension"]),
  };
}

Customers not needing complex serialization logic (e.g., for Extension), can exclude it from their bundle, further optimizing usage.

Pagination Enhancements

Pagination usage aligns with Azure SDK guidelines and offers finer control:

const client = new ServiceClient();
for await (const item of client.listItems()) {
  handleItem(item);
}

// For granular pagination
const previousPage = await client.listItems().byPage().next();
const continuationToken = previousPage.value.continuationToken;
for await (const page of client.listItems().byPage({ continuationToken })) {
  handleItem(page);
}

Long-running Operations (LROs)

Design improvements consolidate polling operations:

  • Old pattern:

    // Wait for completion
    const result = await beginDoSthAndWait();
    // Manual polling
    const poller = await beginDoSth();
    const result = poller.pollUntilDone();
    
  • New pattern:

    // Wait for result
    const result = await doSth();
    // Manual polling
    const poller = doSth();
    await poller.submitted();
    const result = await poller;
    // or
    const result = await poller.pollUntilDone();
    

Summary

The modularized Azure SDK libraries for JavaScript provide:

  • Significant bundle size reductions
  • Modern, composable API imports via subpath exports
  • Comprehensive support for both ESM and CJS
  • Improved usability and flexibility for various scenarios

This approach gives developers robust, performant tools tailored to specific Azure services and app needs.

This post appeared first on “Microsoft DevBlog”. Read the entire article here