次の方法で共有


クイック スタート: TypeSpec と TypeScript を使用して新しい API プロジェクトを作成する

このクイック スタートでは、TypeSpec を使用して RESTful TypeScript API アプリケーションを設計、生成、実装する方法について説明します。 TypeSpec は、クラウド サービス API を記述するためのオープンソース言語であり、複数のプラットフォーム用のクライアントコードとサーバー コードを生成します。 このクイック スタートに従って、API コントラクトを 1 回定義し、一貫した実装を生成する方法について説明します。これにより、保守性が高く、文書化された API サービスを構築できます。

このクイック スタートでは次の作業を行います。

  • TypeSpec を使用して API を定義する
  • API サーバー アプリケーションを作成する
  • Azure Cosmos DB を永続ストレージに統合する
  • Azure にデプロイする
  • API を実行してテストする

Important

@typespec/http-server-js エミッターは現在プレビュー段階です。 この情報は、リリース前に大幅に変更される可能性があるプレリリース製品に関連しています。 Microsoft は、ここに記載されている情報に関して、明示または黙示を問わず、一切の保証を行いません。

Prerequisites

TypeSpec を使用した開発

TypeSpec は、言語に依存しない方法で API を定義し、複数のプラットフォーム用の API サーバーとクライアント ライブラリを生成します。 この機能を利用すると、次のことが可能になります。

  • API コントラクトを 1 回定義する
  • 一貫性のあるサーバーとクライアント のコードを生成する
  • 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で既定のプロジェクトを生成し、2 つの個別のフォルダーを作成します。

    • スキーマ は 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 ファイルを使用して、Express.js サーバー全体をスキャフォールディングするように API サーバーの生成を構成します。

  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: Swagger UI を含む Express.js API サーバーを生成します。
    • 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に移動します。

    ウィジェット API の Swagger UI を表示しているブラウザーのスクリーンショット。

  8. 既定の TypeSpec API とサーバーはどちらも機能します。 この API サーバーを終了する場合は、 ./tsp-output/server/src/controllers/widgets.tsの Widgets API をサポートするビジネス ロジックを追加します。 UI は、ハードコーディングされた偽のデータを返す 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 プロジェクトのファイル構造には、次の Express.js プロジェクトが tsp-outputに含まれています。

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

永続化を Azure Cosmos DB no-sql に変更する

基本的な Express.js API サーバーが動作するように、永続的なデータ ストア用に Azure Cosmos DB と連携するように Express.js サーバーを更新します。 これには、ミドルウェアで Cosmos DB 統合を使用するための index.ts の変更が含まれます。 すべての変更は、 ./tsp-output/server/src/generated ディレクトリの外部で行う必要があります。

  1. ./tsp-output/server ディレクトリで、Azure Cosmos DB をプロジェクトに追加します。

    npm install @azure/cosmos
    
  2. Azure に対して認証する Azure ID ライブラリを追加します。

    npm install @azure/identity
    
  3. Azure に固有のソース コードを保持する ./tsp-output/server/src/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 ID 資格情報 DefaultAzureCredentialを使用しているため、接続文字列やキーは必要ありません。 ローカル環境と運用環境の両方のセキュリティで保護された認証のこの方法の詳細について説明します。

  5. 新しいウィジェット コントローラーを作成し、 ./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 Developer 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 プロジェクトのルートで、Azure Container Apps のコンテナーのビルドに使用する ./Dockerfile を作成します。

    # 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 Developer CLI に対して認証を行います。

    azd auth login
    
  2. Azure Developer 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. Swagger UI を使用するには、エンドポイントにルート /.api-docsを追加します。
  3. API を使用してウィジェットを作成、読み取り、更新、削除するには、各メソッドで 今すぐ試 す機能を使用します。

アプリケーションを拡張する

エンドツーエンドのプロセス全体が機能したら、引き続き API をビルドします。

  • typeSpec 言語の詳細を確認し、./main.tspに API と API レイヤーの機能を追加します。
  • エミッタを追加し、./tspconfig.yamlでそのパラメータを設定します。
  • TypeSpec ファイルにさらに機能を追加する場合は、サーバー プロジェクトのソース コードでそれらの変更をサポートします。
  • Azure ID で パスワードレス認証 を引き続き使用します。

リソースをクリーンアップする

このクイック スタートが完了したら、Azure リソースを削除できます。

azd down

または、Azure portal から直接リソース グループを削除します。

Next steps