共用方式為


快速入門:使用 TypeSpec 和 TypeScript 建立新的 API 專案

在本快速入門中:瞭解如何使用 TypeSpec 來設計、產生及實作 RESTful TypeScript API 應用程式。 TypeSpec 是一種開放原始碼語言,用於描述雲端服務 API,並針對多個平台產生客戶端和伺服器程序代碼。 遵循本快速入門,您將瞭解如何定義 API 合約一次,併產生一致的實作,協助您建置更多可維護且記錄良好的 API 服務。

在本快速入門中,您將:

  • 使用 TypeSpec 定義您的 API
  • 建立 API 伺服器應用程式
  • 整合 Azure Cosmos DB 以實現持久性儲存
  • 部署至 Azure
  • 執行及測試您的 API

Important

@typespec/http-server-js 發出器目前處於預覽狀態。 這項資訊涉及發行前產品,在發行之前可能會有大幅修改。 Microsoft針對此處提供的資訊,不提供任何明示或默示擔保。

Prerequisites

使用 TypeSpec 進行開發

TypeSpec 會以語言無關的方式定義您的 API,並針對多個平台產生 API 伺服器和用戶端連結庫。 此功能讓您可以:

  • 定義 API 合約只需一次即可
  • 產生一致的伺服器和用戶端程序代碼
  • 專注於實作商業規則,而不是 API 基礎結構

TypeSpec 提供 API 服務管理

  • API 定義語言
  • API 的伺服器端路由中間件
  • 取用 API 的用戶端程式庫

您提供用戶端要求和伺服器整合:

  • 在中間件中實作商業規則,例如適用於資料庫、記憶體和傳訊的 Azure 服務
  • 用於託管 API 的伺服器(在本機或 Azure 中)
  • 可重複布建和部署的部署腳本

建立新的 TypeSpec 應用程式

  1. 建立新的資料夾來保存 API 伺服器和 TypeSpec 檔案。

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. 全域安裝 TypeSpec 編譯程式

    npm install -g @typespec/compiler
    
  3. 檢查 TypeSpec 是否已正確安裝:

    tsp --version
    
  4. 初始化 TypeSpec 專案:

    tsp init
    
  5. 回答下列提示,並提供答案:

    • 在這裡初始化新專案嗎? Y
    • 選取項目範本? 一般 REST API
    • 輸入項目名稱:小工具
    • 您要使用哪些排放器?
      • OpenAPI 3.1 檔
      • JavaScript 伺服器存根

    TypeSpec 發出器 是一些利用各種 TypeSpec 編譯器 API 來對 TypeSpec 編譯過程進行分析並生成工件的函式庫。

  6. 等候初始化完成再繼續。

  7. 編譯專案:

    tsp compile .
    
  8. TypeSpec 會在 中 ./tsp-output產生預設專案,並建立兩個不同的資料夾:

    • schema 是 OpenApi 3 規格。 請注意,為您產生的幾行 ./main.tsp 生成了超過 200 行的 OpenApi 規範。
    • 伺服器 是產生的中間件。 此中間件可以併入 Node.js 伺服器專案。
      • ./tsp-output/js/src/generated/models/all/demo-service.ts 定義 Widgets API 的介面。
      • ./tsp-output/js/src/generated/http/openapi3.ts 將Open API規格定義為 TypeScript 檔案,並在每次編譯 TypeSpec 專案時重新產生。

設定 TypeSpec 發射器

使用 TypeSpec 檔案來設定 API 伺服器產生,以建立整個 Express.js 伺服器。

  1. 開啟 ./tsconfig.yaml,並將現有的組態取代為下列 YAML:

    emit:
      - "@typespec/openapi3"
      - "@typespec/http-server-js"
    options:
      "@typespec/openapi3":
        emitter-output-dir: "{output-dir}/server/schema"
        openapi-versions:
          - 3.1.0
      "@typespec/http-server-js":
        emitter-output-dir: "{output-dir}/server"
        express: true
    

    此組態會建立完整的 Express.js API 伺服器:

    • express:產生 Express.js API 伺服器,包括 Swagger UI。
    • emitter-output-dir:將所有內容生成到 ./server 目錄中。
  2. 刪除現有的 ./tsp-output。 別擔心,您將在下一個步驟中產生伺服器。

  3. 使用 TypeSpec JavaScript 發出器來建立 Express.js 伺服器:

    npx hsjs-scaffold
    
  4. 變更為新的 ./tsp-output/server 目錄:

    cd ./tsp-output/server
    
  5. 將 TypeScript 編譯成 JavaScript。

    tsc
    
  6. 執行專案:

    npm start
    

    等候通知後,於瀏覽器中開啟

  7. 開啟瀏覽器並移至 http://localhost:3000/.api-docs

    顯示 Widgets API Swagger UI 的瀏覽器螢幕快照。

  8. 預設的 TypeSpec API 和伺服器都能夠運作。 如果您想要完成此 API 伺服器,請新增商業邏輯以支援 Widgets API 中的 ./tsp-output/server/src/controllers/widgets.ts。 UI 會連線到 API,此 API 會傳回硬式編碼的假數據。

瞭解應用程式檔案結構

tsp-output/server/ 的 Express.js 專案結構包括產生的伺服器、package.json,及 Azure 整合的中介軟體。

server
├── package.json
├── package-lock.json
├── src
│   ├── controllers
│   │   └── widgets.ts
│   ├── generated
│   │   ├── helpers
│   │   │   ├── datetime.ts
│   │   │   ├── header.ts
│   │   │   ├── http.ts
│   │   │   ├── multipart.ts
│   │   │   ├── router.ts
│   │   │   └── temporal
│   │   │       ├── native.ts
│   │   │       └── polyfill.ts
│   │   ├── http
│   │   │   ├── openapi3.ts
│   │   │   ├── operations
│   │   │   │   └── server-raw.ts
│   │   │   └── router.ts
│   │   └── models
│   │       └── all
│   │           ├── demo-service.ts
│   │           └── typespec.ts
│   ├── index.ts
│   └── swagger-ui.ts

父 TypeSpec 專案的檔案結構包括位於 tsp-output 中的這個 Express.js 專案:

├── tsp-output
├── .gitignore
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml

切換至 Azure Cosmos DB NoSQL 來實現資料持久性

基本的 Express.js API 伺服器現已運作,現在請更新 Express.js 伺服器,以將 Azure Cosmos DB 設為持久性資料存放庫。 這包括變更index.ts,以便在中間件中使用 Cosmos DB 整合。 所有變更都應該發生在目錄外部 ./tsp-output/server/src/generated

  1. ./tsp-output/server 目錄中,將 Azure Cosmos DB 新增至專案:

    npm install @azure/cosmos
    
  2. 新增 Azure 身分識別連結庫向 Azure 進行驗證

    npm install @azure/identity
    
  3. 建立 ./tsp-output/server/src/azure 目錄來保存 Azure 專屬的原始程式碼。

  4. 在該 cosmosClient.ts 目錄中建立檔案以建立Cosmos DB客戶端物件,並貼上下列程式代碼:

    import { CosmosClient, Database, Container } from "@azure/cosmos";
    import { DefaultAzureCredential } from "@azure/identity";
    
    /**
     * Interface for CosmosDB configuration settings
     */
    export interface CosmosConfig {
      endpoint: string;
      databaseId: string;
      containerId: string;
      partitionKey: string;
    } 
    
    /**
     * Singleton class for managing CosmosDB connections
     */
    export class CosmosClientManager {
      private static instance: CosmosClientManager;
      private client: CosmosClient | null = null;
      private config: CosmosConfig | null = null;
    
      private constructor() {}
    
      /**
       * Get the singleton instance of CosmosClientManager
       */
      public static getInstance(): CosmosClientManager {
        if (!CosmosClientManager.instance) {
          CosmosClientManager.instance = new CosmosClientManager();
        }
        return CosmosClientManager.instance;
      }
    
      /**
       * Initialize the CosmosDB client with configuration if not already initialized
       * @param config CosmosDB configuration
       */
      private ensureInitialized(config: CosmosConfig): void {
        if (!this.client || !this.config) {
          this.config = config;
          this.client = new CosmosClient({
            endpoint: config.endpoint,
            aadCredentials: new DefaultAzureCredential(),
          });
        }
      }
    
      /**
       * Get a database instance, creating it if it doesn't exist
       * @param config CosmosDB configuration
       * @returns Database instance
       */
      private async getDatabase(config: CosmosConfig): Promise<Database> {
        this.ensureInitialized(config);
        const { database } = await this.client!.databases.createIfNotExists({ id: config.databaseId });
        return database;
      }
    
      /**
       * Get a container instance, creating it if it doesn't exist
       * @param config CosmosDB configuration
       * @returns Container instance
       */
      public async getContainer(config: CosmosConfig): Promise<Container> {
        const database = await this.getDatabase(config);
        const { container } = await database.containers.createIfNotExists({
          id: config.containerId,
          partitionKey: { paths: [config.partitionKey] }
        });
        return container;
      }
    
      /**
       * Clean up resources and close connections
       */
      public dispose(): void {
        this.client = null;
        this.config = null;
      }
    }
    
    export const buildError = (error: any, message: string) => {
      const statusCode = error?.statusCode || 500;
      return {
        code: statusCode,
        message: `${message}: ${error?.message || 'Unknown error'}`
      };
    };
    

    請注意,檔案會使用端點、資料庫和容器。 它不需要連接字串或金鑰,因為它使用 Azure 身分識別認證 DefaultAzureCredential。 深入瞭解這種用於本機和生產環境的安全驗證方法。

  5. 建立新的 Widget 控制器, ./tsp-output/server/src/controllers/WidgetsCosmos.ts並貼上下列 Azure Cosmos DB 整合程序代碼。

    import { Widgets, Widget, WidgetList,   AnalyzeResult,Error } from "../generated/models/all/demo-service.js";
    import { WidgetMergePatchUpdate } from "../generated/models/all/typespec/http.js";
    import { CosmosClientManager, CosmosConfig, buildError } from "../azure/cosmosClient.js";
    import { HttpContext } from "../generated/helpers/router.js";
    import { Container } from "@azure/cosmos";
    
    export interface WidgetDocument extends Widget {
      _ts?: number;
      _etag?: string;
    }
    
    /**
     * Implementation of the Widgets API using Azure Cosmos DB for storage
     */
    export class WidgetsCosmosController implements Widgets<HttpContext>  {
      private readonly cosmosConfig: CosmosConfig;
      private readonly cosmosManager: CosmosClientManager;
      private container: Container | null = null;
    
      /**
       * Creates a new instance of WidgetsCosmosController
       * @param azureCosmosEndpoint Cosmos DB endpoint URL
       * @param databaseId The Cosmos DB database ID
       * @param containerId The Cosmos DB container ID
       * @param partitionKey The partition key path
       */
      constructor(azureCosmosEndpoint: string, databaseId: string, containerId: string, partitionKey: string) {
        if (!azureCosmosEndpoint) throw new Error("azureCosmosEndpoint is required");
        if (!databaseId) throw new Error("databaseId is required");
        if (!containerId) throw new Error("containerId is required");
        if (!partitionKey) throw new Error("partitionKey is required");
    
        this.cosmosConfig = {
          endpoint: azureCosmosEndpoint,
          databaseId: databaseId,
          containerId: containerId,
          partitionKey: partitionKey
        };
    
        this.cosmosManager = CosmosClientManager.getInstance();
      }
    
      /**
       * Get the container reference, with caching
       * @returns The Cosmos container instance
       */
      private async getContainer(): Promise<Container | null> {
        if (!this.container) {
          try {
            this.container = await this.cosmosManager.getContainer(this.cosmosConfig);
            return this.container;
          } catch (error: any) {
            console.error("Container initialization error:", error);
            throw buildError(error, `Failed to access container ${this.cosmosConfig.containerId}`);
          }
        }
        return this.container;
      }
    
      /**
       * Create a new widget
       * @param widget The widget to create
       * @returns The created widget with assigned ID
       */
      async create(ctx: HttpContext,
        body: Widget
      ): Promise<Widget | Error> {
    
        const id = body.id;
    
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          if (!body.id) {
            return buildError({statusCode:400}, "Widget ID is required");
          }
    
          const response = await container.items.create<Widget>(body, { 
            disableAutomaticIdGeneration: true 
          });
    
          if (!response.resource) {
            return buildError({statusCode:500}, `Failed to create widget ${body.id}: No resource returned`);
          }
    
          return this.documentToWidget(response.resource);
        } catch (error: any) {
          if (error?.statusCode === 409) {
            return buildError({statusCode:409}, `Widget with id ${id} already exists`);
          }
          return buildError(error, `Failed to create widget ${id}`);
        }
      }
    
      /**
       * Delete a widget by ID
       * @param id The ID of the widget to delete
       */
      async delete(ctx: HttpContext, id: string): Promise<void | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          await container.item(id, id).delete();
        } catch (error: any) {
          if (error?.statusCode === 404) {
            return buildError({statusCode:404}, `Widget with id ${id} not found`);
          }
          return buildError(error, `Failed to delete widget ${id}`);
        }
      }
    
      /**
       * Get a widget by ID
       * @param id The ID of the widget to retrieve
       * @returns The widget if found
       */
      async read(ctx: HttpContext, id: string): Promise<Widget | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          const { resource } = await container.item(id, id).read<WidgetDocument>();
    
          if (!resource) {
            return buildError({statusCode:404}, `Widget with id ${id} not found`);
          }
    
          return this.documentToWidget(resource);
        } catch (error: any) {
          return buildError(error, `Failed to read widget ${id}`);
        }
      }
    
      /**
       * List all widgets with optional paging
       * @returns List of widgets
       */
      async list(ctx: HttpContext): Promise<WidgetList | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          const { resources } = await container.items
            .query({ query: "SELECT * FROM c" })
            .fetchAll();
    
          return { items: resources.map(this.documentToWidget) };
        } catch (error: any) {
          return buildError(error, "Failed to list widgets");
        }
      }
    
      /**
       * Update an existing widget
       * @param id The ID of the widget to update
       * @param body The partial widget data to update
       * @returns The updated widget
       */
      async update(
        ctx: HttpContext,
        id: string,
        body: WidgetMergePatchUpdate,
      ): Promise<Widget | Error> {
        try {
          const container = await this.getContainer();
    
          if(!container) {
            return buildError({statusCode:500}, "Container is not initialized");
          }
    
          // First check if the widget exists
          const { resource: item } = await container.item(id).read<WidgetDocument>();
          if (!item) {
            return buildError({statusCode:404}, `Widget with id ${id} not found`);
          }
    
          // Apply patch updates to the existing widget
          const updatedWidget: Widget = {
            ...item,
            ...body,
            id
          };
    
          // Replace the document in Cosmos DB
          const { resource } = await container.item(id).replace(updatedWidget);
    
          if (!resource) {
            return buildError({statusCode:500}, `Failed to update widget ${id}: No resource returned`);
          }
    
          return this.documentToWidget(resource);
        } catch (error: any) {
          return buildError(error, `Failed to update widget ${id}`);
        }
      }
    
      async analyze(ctx: HttpContext, id: string): Promise<AnalyzeResult | Error> {
        return {
          id: "mock-string",
          analysis: "mock-string",
        };
      }
    
      /**
       * Convert a Cosmos DB document to a Widget
       */
      private documentToWidget(doc: WidgetDocument): Widget {
        return Object.fromEntries(
          Object.entries(doc).filter(([key]) => !key.startsWith('_'))
        ) as Widget;
      }
    }
    
  6. 更新 ./tsp-output/server/src/index.ts 以匯入新的控制器、取得 Azure Cosmos DB 的環境設定,然後建立 WidgetsCosmosController 並將其傳遞至路由器。

    // Generated by Microsoft TypeSpec
    
    import { WidgetsCosmosController } from "./controllers/WidgetsCosmos.js";
    
    import { createDemoServiceRouter } from "./generated/http/router.js";
    
    import express from "express";
    
    import morgan from "morgan";
    
    import { addSwaggerUi } from "./swagger-ui.js";
    
    const azureCosmosEndpoint = process.env.AZURE_COSMOS_ENDPOINT!;
    const azureCosmosDatabase = "WidgetDb";
    const azureCosmosContainer = "Widgets";
    const azureCosmosPartitionKey = "/Id";
    
    const router = createDemoServiceRouter(
      new WidgetsCosmosController(
        azureCosmosEndpoint, 
        azureCosmosDatabase, 
        azureCosmosContainer, 
        azureCosmosPartitionKey)
    );
    const PORT = process.env.PORT || 3000;
    
    const app = express();
    
    app.use(morgan("dev"));
    
    const SWAGGER_UI_PATH = process.env.SWAGGER_UI_PATH || "/.api-docs";
    
    addSwaggerUi(SWAGGER_UI_PATH, app);
    
    app.use(router.expressMiddleware);
    
    app.listen(PORT, () => {
      console.log(`Server is running at http://localhost:${PORT}`);
      console.log(
        `API documentation is available at http://localhost:${PORT}${SWAGGER_UI_PATH}`,
      );
    });
    
  7. 在的 ./tsp-output/server終端機中,將 TypeScript 編譯成 JavaScript。

    tsc
    

    項目現在會使用 Cosmos DB 整合來建置。 讓我們建立部署腳本來建立 Azure 資源並部署專案。

建立部署基礎結構

使用 Azure 開發人員 CLIBicep 範本建立可重複部署所需的檔案。

  1. 在 TypeSpec 專案的根目錄中,建立 azure.yaml 部署定義檔,並貼上下列來源:

    # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
    
    name: azure-typespec-scaffold-js
    metadata:
        template: azd-init@1.14.0
    services:
        api:
            project: ./
            host: containerapp
            language: js
            docker:
                path: Dockerfile
    pipeline:
      provider: github
    hooks:
      postprovision:
        windows:
          shell: pwsh
          run: |
            # Set environment variables for the Container App
            azd env set AZURE_COSMOS_ENDPOINT "$env:AZURE_COSMOS_ENDPOINT"
          continueOnError: false
          interactive: true
        posix:
          shell: sh
          run: |
            # Set environment variables for the Container App
            azd env set AZURE_COSMOS_ENDPOINT "$AZURE_COSMOS_ENDPOINT"
          continueOnError: false
          interactive: true
    

    請注意,此組態會參考整個 TypeSpec 專案。

  2. 在 TypeSpec 專案的根目錄中,建立 ./Dockerfile,用來建置 Azure Container Apps 的容器。

    # Stage 1: Build stage
    FROM node:20-alpine AS builder
    
    WORKDIR /app
    
    # Install TypeScript globally
    RUN npm install -g typescript
    
    # Copy package files first to leverage Docker layer caching
    COPY package*.json ./
    
    # Create the tsp-output/server directory structure
    RUN mkdir -p tsp-output/server
    
    # Copy server package.json 
    COPY tsp-output/server/package.json ./tsp-output/server/
    
    # Install build and dev dependencies
    RUN npm i --force --no-package-lock
    RUN cd tsp-output/server && npm install
    
    # Copy the rest of the application code
    COPY . .
    
    # Build the TypeScript code
    RUN cd tsp-output/server && tsc
    
    #---------------------------------------------------------------
    
    # Stage 2: Runtime stage
    FROM node:20-alpine AS runtime
    
    # Set NODE_ENV to production for better performance
    ENV NODE_ENV=production
    
    WORKDIR /app
    
    # Copy only the server package files
    COPY tsp-output/server/package.json ./
    
    # Install only production dependencies
    RUN npm install
    
    # Copy all necessary files from the builder stage
    # This includes the compiled JavaScript, any static assets, etc.
    COPY --from=builder /app/tsp-output/server/dist ./dist
    
    # Set default port and expose it
    ENV PORT=3000
    EXPOSE 3000
    
    # Run the application
    CMD ["node", "./dist/src/index.js"]
    
  3. 在 TypeSpec 專案的根目錄中,建立 ./infra 目錄。

  4. 建立一個 ./infra/main.bicepparam 檔案,並將以下內容複製進去,以定義我們部署所需的參數:

    using './main.bicep'
    
    param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'dev')
    param location = readEnvironmentVariable('AZURE_LOCATION', 'eastus2')
    param deploymentUserPrincipalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', '')
    

    此參數清單提供此部署所需的最低參數。

  5. 建立 ./infra/main.bicep 檔案並將以下內容複製到檔案中,以定義用於布建和部署的 Azure 資源:

    metadata description = 'Bicep template for deploying a GitHub App using Azure Container Apps and Azure Container Registry.'
    
    targetScope = 'resourceGroup'
    param serviceName string = 'api'
    var databaseName = 'WidgetDb'
    var containerName = 'Widgets'
    var partitionKey = '/id'
    
    @minLength(1)
    @maxLength(64)
    @description('Name of the environment that can be used as part of naming resource convention')
    param environmentName string
    
    @minLength(1)
    @description('Primary location for all resources')
    param location string
    
    @description('Id of the principal to assign database and application roles.')
    param deploymentUserPrincipalId string = ''
    
    var resourceToken = toLower(uniqueString(resourceGroup().id, environmentName, location))
    
    var tags = {
      'azd-env-name': environmentName
      repo: 'https://github.com/typespec'
    }
    
    module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
      name: 'user-assigned-identity'
      params: {
        name: 'identity-${resourceToken}'
        location: location
        tags: tags
      }
    }
    
    module cosmosDb 'br/public:avm/res/document-db/database-account:0.8.1' = {
      name: 'cosmos-db-account'
      params: {
        name: 'cosmos-db-nosql-${resourceToken}'
        location: location
        locations: [
          {
            failoverPriority: 0
            locationName: location
            isZoneRedundant: false
          }
        ]
        tags: tags
        disableKeyBasedMetadataWriteAccess: true
        disableLocalAuth: true
        networkRestrictions: {
          publicNetworkAccess: 'Enabled'
          ipRules: []
          virtualNetworkRules: []
        }
        capabilitiesToAdd: [
          'EnableServerless'
        ]
        sqlRoleDefinitions: [
          {
            name: 'nosql-data-plane-contributor'
            dataAction: [
              'Microsoft.DocumentDB/databaseAccounts/readMetadata'
              'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
              'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
            ]
          }
        ]
        sqlRoleAssignmentsPrincipalIds: union(
          [
            managedIdentity.outputs.principalId
          ],
          !empty(deploymentUserPrincipalId) ? [deploymentUserPrincipalId] : []
        )
        sqlDatabases: [
          {
            name: databaseName
            containers: [
              {
                name: containerName
                paths: [
                  partitionKey
                ]
              }
            ]
          }
        ]
      }
    }
    
    module containerRegistry 'br/public:avm/res/container-registry/registry:0.5.1' = {
      name: 'container-registry'
      params: {
        name: 'containerreg${resourceToken}'
        location: location
        tags: tags
        acrAdminUserEnabled: false
        anonymousPullEnabled: true
        publicNetworkAccess: 'Enabled'
        acrSku: 'Standard'
      }
    }
    
    var containerRegistryRole = subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      '8311e382-0749-4cb8-b61a-304f252e45ec'
    ) 
    
    module registryUserAssignment 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.1' = if (!empty(deploymentUserPrincipalId)) {
      name: 'container-registry-role-assignment-push-user'
      params: {
        principalId: deploymentUserPrincipalId
        resourceId: containerRegistry.outputs.resourceId
        roleDefinitionId: containerRegistryRole
      }
    }
    
    module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = {
      name: 'log-analytics-workspace'
      params: {
        name: 'log-analytics-${resourceToken}'
        location: location
        tags: tags
      }
    }
    
    module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.8.0' = {
      name: 'container-apps-env'
      params: {
        name: 'container-env-${resourceToken}'
        location: location
        tags: tags
        logAnalyticsWorkspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
        zoneRedundant: false
      }
    }
    
    module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = {
      name: 'container-apps-app'
      params: {
        name: 'container-app-${resourceToken}'
        environmentResourceId: containerAppsEnvironment.outputs.resourceId
        location: location
        tags: union(tags, { 'azd-service-name': serviceName })
        ingressTargetPort: 3000
        ingressExternal: true
        ingressTransport: 'auto'
        stickySessionsAffinity: 'sticky'
        scaleMaxReplicas: 1
        scaleMinReplicas: 1
        corsPolicy: {
          allowCredentials: true
          allowedOrigins: [
            '*'
          ]
        }
        managedIdentities: {
          systemAssigned: false
          userAssignedResourceIds: [
            managedIdentity.outputs.resourceId
          ]
        }
        secrets: {
          secureList: [
            {
              name: 'azure-cosmos-db-nosql-endpoint'
              value: cosmosDb.outputs.endpoint
            }
            {
              name: 'user-assigned-managed-identity-client-id'
              value: managedIdentity.outputs.clientId
            }
          ]
        }
        containers: [
          {
            image: 'mcr.microsoft.com/devcontainers/typescript-node'
            name: serviceName
            resources: {
              cpu: '0.25'
              memory: '.5Gi'
            }
            env: [
              {
                name: 'AZURE_COSMOS_ENDPOINT'
                secretRef: 'azure-cosmos-db-nosql-endpoint'
              }
              {
                name: 'AZURE_CLIENT_ID'
                secretRef: 'user-assigned-managed-identity-client-id'
              }
            ]
          }
        ]
      }
    }
    
    output AZURE_COSMOS_ENDPOINT string = cosmosDb.outputs.endpoint
    output AZURE_COSMOS_DATABASE string = databaseName
    output AZURE_COSMOS_CONTAINER string = containerName
    output AZURE_COSMOS_PARTITION_KEY string = partitionKey
    
    output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
    output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name
    

    OUTPUT 變數可讓您在本機開發時使用已布建的雲端資源。

將應用程式部署至 Azure

您可以使用 Azure Container Apps 將此應用程式部署至 Azure:

  1. 在專案根目錄的終端機中,向 Azure 開發人員 CLI 進行驗證:

    azd auth login
    
  2. 使用 Azure 開發人員 CLI 部署至 Azure Container Apps:

    azd up
    
  3. 回答下列提示,並提供答案:

    • 輸入唯一的環境名稱: tsp-server-js
    • 選取要使用的 Azure 訂用帳戶:選取您的訂用帳戶
    • 選取要使用的 Azure 位置:選取您附近的位置
    • 挑選要使用的資源群組:選取 [建立新的資源群組]
    • 輸入新資源群組的名稱:接受所提供的預設值
  4. 等到部署完成為止。 回應包含類似下列的資訊:

    Deploying services (azd deploy)
    
      (✓) Done: Deploying service api
      - Endpoint: https://container-app-123.ambitiouscliff-456.centralus.azurecontainerapps.io/
    
    
    SUCCESS: Your up workflow to provision and deploy to Azure completed in 6 minutes 32 seconds.
    

在瀏覽器中使用應用程式

部署之後,您可以:

  1. 在控制台中,選取 Endpoint URL以在瀏覽器中開啟它。
  2. 將路由 /.api-docs新增至端點,以使用 Swagger UI。
  3. 使用每個方法上的 [立即試用] 功能,透過 API 建立、讀取、更新和刪除小工具。

拓展您的應用程式

既然您已讓整個端對端程式正常運作,請繼續建置您的 API:

  • 深入瞭解 TypeSpec 語言 ,以在 中 ./main.tsp新增更多 API 和 API 層功能。
  • 中新增更多./tspconfig.yaml並配置其參數。
  • 當您在 TypeSpec 檔案中新增更多功能時,請在伺服器專案中使用原始程式碼支援這些變更。
  • 繼續使用 Azure 身分識別的無密碼驗證

清理資源

完成本快速入門后,您可以移除 Azure 資源:

azd down

或者直接從 Azure 入口網站刪除資源群組。

後續步驟