Skip to content

Express Backend

Setup

To access the Google APIs in the backend, first install googleapis:

sh
npm i googleapis

Authentication

The Express demo requests a scoped access token from the platform and then creates a Google client with that token. The following example shows a google authentication handler:

googleApiService.ts
ts
class GoogleAuthService {
  /**
   * Creates and configures a Google OAuth2 client with a platform-provided access token.
   *
   * Uses the shared TokenService instead of calling the token endpoint directly.
   *
   * @param req - The Express request object containing the Authorization header
   * @param scopes - The scopes required for the Google API client
   * @returns Configured OAuth2 client for Google APIs
   */
  static async getGoogleAuth(req: express.Request, scopes: string) {
    const token = await TokenService.getToken("google", scopes);

    const auth = new google.auth.OAuth2();
    auth.setCredentials({ access_token: token });
    return auth;
  }
}

This auth handler can obtain tokens with dynamic scopes. It can be used to create any Google client, such as google.drive, google.calendar, or google.docs. Note how the GoogleAuthService uses the TokenService to obtain the token. The TokenService is part of the express template and is preferrable to using a self-implemented solution. Furthermore, GoogleAuthService in this example is parameterized, allowing the acquisition of tokens with dynamic scopes.

Usage

The googleapis library for express offers built-in OAuth2 support, types and easy Access to Google APIs. This example shows an authentication handler built using googleapis:

googleApiService.ts
ts
class GoogleAuthService {
  /**
   * Creates and configures a Google OAuth2 client with a platform-provided access token.
   *
   * Uses the shared TokenService instead of calling the token endpoint directly.
   *
   * @param req - The Express request object containing the Authorization header
   * @param scopes - The scopes required for the Google API client
   * @returns Configured OAuth2 client for Google APIs
   */
  static async getGoogleAuth(req: express.Request, scopes: string) {
    const token = await TokenService.getToken("google", scopes);

    const auth = new google.auth.OAuth2();
    auth.setCredentials({ access_token: token });
    return auth;
  }
}

Then you can create a Google client using the auth handler. The following example shows a simple Google Calendar client:

devOpsService.ts
ts
import { TokenService } from "../../lib/services/tokenService";

/**
 * Request body for the Azure DevOps Work Items Batch endpoint.
 *
 * @see https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/get-work-items-batch
 */
export interface WorkItemBatchRequest {
  /** The requested work item IDs (maximum 200) */
  ids: number[];
  /** The requested fields */
  fields?: string[];
  /** The expand parameters for work item attributes: None, Relations, Fields, Links, All */
  $expand?: string;
  /** AsOf UTC date time string */
  asOf?: string;
  /** The flag to control error policy: Fail, Omit */
  errorPolicy?: string;
}

/**
 * A single work item returned by the batch endpoint.
 */
export interface WorkItem {
  id: number;
  rev: number;
  fields: Record<string, any>;
  url: string;
  _links?: any;
  relations?: any[];
  commentVersionRef?: any;
}

/**
 * Response from the Azure DevOps Work Items Batch endpoint.
 */
export interface WorkItemBatchResponse {
  count: number;
  value: WorkItem[];
}

/**
 * Request body for the WIQL query endpoint.
 */
export interface WiqlRequest {
  /** The text of the WIQL query */
  query: string;
}

/**
 * The result type of a work item query.
 */
export type QueryResultType = "workItem" | "workItemLink";

/**
 * The type of query: flat, tree, or oneHop.
 */
export type QueryType = "flat" | "tree" | "oneHop";

/**
 * Reference to a field in a work item.
 */
export interface WorkItemFieldReference {
  name: string;
  referenceName: string;
  url: string;
}

/**
 * A sort column in a work item query.
 */
export interface WorkItemQuerySortColumn {
  descending: boolean;
  field: WorkItemFieldReference;
}

/**
 * Contains reference to a work item.
 */
export interface WorkItemReference {
  id: number;
  url: string;
}

/**
 * A link between two work items.
 */
export interface WorkItemLink {
  rel: string;
  source: WorkItemReference;
  target: WorkItemReference;
}

/**
 * The result of a work item query.
 */
export interface WorkItemQueryResult {
  asOf: string;
  columns: WorkItemFieldReference[];
  queryResultType: QueryResultType;
  queryType: QueryType;
  sortColumns: WorkItemQuerySortColumn[];
  workItemRelations?: WorkItemLink[];
  workItems: WorkItemReference[];
}

export class DevOpsService {
  private static readonly AZURE_DEVOPS_BASE_URL = "https://dev.azure.com";
  private static readonly API_VERSION_BATCH = "7.2-preview.1";
  private static readonly API_VERSION_WIQL = "7.2-preview.2";
  private static readonly SCOPES =
    "499b84ac-1321-427f-aa17-267ca6975798/user_impersonation";

  /**
   * Gets work items for a list of work item IDs (maximum 200).
   *
   * Calls the Azure DevOps REST API directly (not via the platform gateway).
   * The OAuth access token is obtained through the platform token endpoint.
   * If the user has not consented to the required scopes, the platform returns
   * a 403 with a Location header pointing to the OAuth consent URL, which is
   * automatically forwarded to the frontend by the error handler.
   *
   * @param organization - Azure DevOps organization name
   * @param request - Batch request body with work item IDs and optional fields/expand
   * @param project - Optional project name or ID to scope the request
   * @returns Batch response containing the requested work items
   */
  static async getWorkItemsBatch(
    organization: string,
    request: WorkItemBatchRequest,
    project?: string,
  ): Promise<WorkItemBatchResponse> {
    //#region devOpsServiceCalling
    // Obtain the Azure DevOps OAuth token via the platform token endpoint.
    // If the principal has not consented, this throws UpstreamHTTPError(403)
    // with a Location header, which the Express error handler forwards to the frontend.
    const token = await TokenService.getToken("default", DevOpsService.SCOPES);

    // Build the direct Azure DevOps REST API URL
    const projectPath = project ? `/${encodeURIComponent(project)}` : "";
    const url = `${DevOpsService.AZURE_DEVOPS_BASE_URL}/${encodeURIComponent(organization)}${projectPath}/_apis/wit/workitemsbatch?api-version=${DevOpsService.API_VERSION_BATCH}`;

    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(request),
    });

    if (!response.ok) {
      throw new Error(
        `Failed to get work items batch: ${response.status} ${response.statusText}`,
      );
    }

    const data = (await response.json()) as WorkItemBatchResponse;
    //#endregion devOpsServiceCalling
    return data;
  }

  /**
   * Gets the results of a WIQL query.
   *
   * Executes a Work Item Query Language (WIQL) query against Azure DevOps
   * and returns the matching work item references. The results can then be
   * used with {@link getWorkItemsBatch} to fetch full work item details.
   *
   * Calls the Azure DevOps REST API directly (not via the platform gateway).
   * The OAuth access token is obtained through the platform token endpoint.
   * If the user has not consented to the required scopes, the platform returns
   * a 403 with a Location header pointing to the OAuth consent URL, which is
   * automatically forwarded to the frontend by the error handler.
   *
   * @param organization - Azure DevOps organization name
   * @param request - WIQL query request body with the query text
   * @param project - Optional project name or ID to scope the query
   * @param team - Optional team ID or name
   * @param top - Optional max number of results to return
   * @param timePrecision - Optional whether to use time precision
   * @returns Query result containing matching work item references
   */
  static async queryByWiql(
    organization: string,
    request: WiqlRequest,
    project?: string,
    team?: string,
    top?: number,
    timePrecision?: boolean,
  ): Promise<WorkItemQueryResult> {
    // Obtain the Azure DevOps OAuth token via the platform token endpoint.
    // If the principal has not consented, this throws UpstreamHTTPError(403)
    // with a Location header, which the Express error handler forwards to the frontend.
    const token = await TokenService.getToken("default", DevOpsService.SCOPES);

    // Build the direct Azure DevOps REST API URL
    const projectPath = project ? `/${encodeURIComponent(project)}` : "";
    const teamPath = team ? `/${encodeURIComponent(team)}` : "";

    const params = new URLSearchParams({
      "api-version": DevOpsService.API_VERSION_WIQL,
    });
    if (top !== undefined) {
      params.append("$top", String(top));
    }
    if (timePrecision !== undefined) {
      params.append("timePrecision", String(timePrecision));
    }

    const url = `${DevOpsService.AZURE_DEVOPS_BASE_URL}/${encodeURIComponent(organization)}${projectPath}${teamPath}/_apis/wit/wiql?${params.toString()}`;

    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(request),
    });

    if (!response.ok) {
      throw new Error(
        `Failed to query by WIQL: ${response.status} ${response.statusText}`,
      );
    }

    const data = (await response.json()) as WorkItemQueryResult;
    return data;
  }
}