このクイック スタートでは、TypeSpec を使用して RESTful TypeScript API アプリケーションを設計、生成、実装する方法について説明します。 TypeSpec は、クラウド サービス API を記述するためのオープンソース言語であり、複数のプラットフォーム用のクライアントコードとサーバー コードを生成します。 このクイック スタートに従って、API コントラクトを 1 回定義し、一貫した実装を生成する方法について説明します。これにより、保守性が高く、文書化された API サービスを構築できます。
TypeSpec のコード ジェネレーターを使用して、ルーティングと Swagger UI を使用して Express.js サーバーをスキャフォールディングし、永続化のために Azure Cosmos DB に接続し、Azure Container Apps にデプロイします。
API 開発における TypeSpec のロールのコンテキストについては、「 TypeSpec の概要」を参照してください。
完了までの時間: 15 ~ 20 分
このクイック スタートでは次の作業を行います。
- TypeSpec を使用して API を定義する
- API サーバー アプリケーションを作成する
- Azure Cosmos DB を永続ストレージに統合する
- Azure にデプロイする
- API を実行してテストする
Important
@typespec/http-server-js エミッターは現在プレビュー段階です。
この情報は、リリース前に大幅に変更される可能性があるプレリリース製品に関連しています。 Microsoft は、ここに記載されている情報に関して、明示または黙示を問わず、一切の保証を行いません。
Prerequisites
- アクティブな Azure アカウントアカウントがない場合、Azure 試用版にサインアップして、最大 10 件の無料 Mobile Apps を入手できます。 アカウントがない場合は、無料でアカウントを作成 します。
- Node.js LTS — TypeSpec CLI とパッケージ マネージャーに必要です。
- TypeScript コードを記述およびコンパイルするための TypeScript。
- Docker
- Visual Studio Code
- TypeSpec 拡張機能
- 省略可能: Azure Developer CLI を使用したデプロイ
TypeSpec を使用した開発
TypeSpec は、言語に依存しない方法で API を定義し、複数のプラットフォーム用の API サーバーとクライアント ライブラリを生成します。 この機能を利用すると、次のことが可能になります。
- API コントラクトを 1 回定義する
- 一貫性のあるサーバーとクライアント のコードを生成する
- API インフラストラクチャではなくビジネス ロジックの実装に重点を置く
TypeSpec は API サービス管理を提供します。
- API 定義言語
- API 用のサーバー側ルーティング ミドルウェア
- API を使用するためのクライアント ライブラリ
クライアント要求とサーバー統合を指定します。
- データベース、ストレージ、メッセージング用の Azure サービスなどのミドルウェアにビジネス ロジックを実装する
- API のホスティング サーバー (ローカルまたは Azure)
- 繰り返し可能なプロビジョニングとデプロイのためのスクリプト
新しい TypeSpec アプリケーションを作成する
API サーバーと TypeSpec ファイルを保持する新しいフォルダーを作成します。
mkdir my_typespec_quickstart cd my_typespec_quickstartTypeSpec コンパイラをグローバルにインストールします。
npm install -g @typespec/compilerTypeSpec が正しくインストールされていることを確認します。
tsp --versionTypeSpec プロジェクトを初期化します。
tsp init次のプロンプトには提供された答えで回答してください。
- ここで新しいプロジェクトを初期化しますか? Y
- プロジェクト テンプレートを選択しますか? 汎用 REST API
- プロジェクト名を入力します:ウィジェット
- どのエミッターを使用しますか?
- OpenAPI 3.1 ドキュメント
- JavaScript サーバー スタブ
TypeSpec エミッタ ーは、さまざまな TypeSpec コンパイラ API を利用して TypeSpec コンパイル プロセスを反映し、成果物を生成するライブラリです。
ヒント
非対話型セットアップの場合は、
tsp initとプロンプトの応答をtsp init --template rest --project-name Widgets --template-emitters @typespec/openapi3 --template-emitters @typespec/http-server-js --no-promptに置き換えます。 JavaScript サーバー エミッターを指定すると、プロジェクトに@typespec/http-server-jsが確実に含まれます。初期化が完了するまで待ってから続行します。
Note
tsp initは、プロジェクトの依存関係をインストールします。 プロジェクト ファイルの作成後に依存関係のインストールが失敗した場合は、TypeSpec プロジェクト ルートからnpm install実行してから続行します。プロジェクトをコンパイルします。
tsp compile .ヒント
反復開発の場合は、ウォッチ モードを使用して、ファイルの変更 (
tsp compile . --watch) を自動的に再コンパイルします。 これにより、main.tspを変更すると、生成されたサーバーとスキーマが最新の状態に保たれます。TypeSpec は、
./tsp-outputで既定のプロジェクトを生成し、2 つの個別のフォルダーを作成します。-
スキーマ は OpenApi 3 仕様です。
./main.tspの数行が、あなたのために200行を超えるOpenApi仕様を生成していることに注意してください。 -
サーバー は生成されたミドルウェアです。 このミドルウェアは、Node.js サーバー プロジェクトに組み込むことができます。
-
./tsp-output/server/js/src/generated/models/all/demo-service.tsは Widgets API のインターフェイスを定義します。 -
./tsp-output/server/js/src/generated/http/openapi3.tsでは、Open API 仕様が TypeScript ファイルとして定義され、TypeSpec プロジェクトをコンパイルするたびに再生成されます。
-
-
スキーマ は OpenApi 3 仕様です。
TypeSpec エミッタを構成する
TypeSpec ファイルを使用して、Express.js サーバー全体をスキャフォールディングするように API サーバーの生成を構成します。
./tspconfig.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ディレクトリに生成します。
-
既存の
./tsp-outputを削除します。 心配しないで、次の手順でサーバーを生成します。TypeSpec JavaScript エミッタを使用して、Express.js サーバーを作成します。
npx hsjs-scaffold新しい
./tsp-output/serverディレクトリに移動します。cd ./tsp-output/serverTypeScript を JavaScript にコンパイルします。
tscプロジェクトを実行します。
npm start通知が ブラウザーで開くのを待ちます。
ブラウザーを開き、
http://localhost:3000/.api-docsに移動します。
既定の 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 ディレクトリの外部で行う必要があります。
./tsp-output/serverディレクトリで、Azure Cosmos DB をプロジェクトに追加します。npm install @azure/cosmosAzure に対して認証する Azure ID ライブラリを追加します。
npm install @azure/identityAzure に固有のソース コードを保持する
./tsp-output/server/src/azureディレクトリを作成します。そのディレクトリに
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を使用しているため、接続文字列やキーは必要ありません。 ローカル環境と運用環境の両方のセキュリティで保護された認証のこの方法の詳細について説明します。新しいウィジェット コントローラーを作成し、
./tsp-output/server/src/controllers/WidgetsCosmos.tsし、Azure Cosmos DB の次の統合コードを貼り付けます。import { Widgets, Widget, WidgetList, AnalyzeResult, Error, WidgetMergePatchUpdate } from "../generated/models/all/demo-service.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; } }./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}`, ); });./tsp-output/serverのターミナルで、TypeScript を JavaScript にコンパイルします。tscこれで、プロジェクトは Cosmos DB 統合でビルドされます。 Azure リソースを作成してプロジェクトをデプロイするデプロイ スクリプトを作成しましょう。
デプロイ インフラストラクチャを作成する
Azure Developer CLI と Bicep テンプレートを使用して、繰り返し可能なデプロイを行うために必要なファイルを作成します。
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 プロジェクト全体を参照していることに注意してください。
TypeSpec プロジェクトのルートで、Azure Container Apps のコンテナーのビルドに使用する
./Dockerfileを作成します。# Stage 1: Build stage FROM node:lts-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:lts-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"]TypeSpec プロジェクトのルートで、
./infraディレクトリを作成します。./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', '')このパラメーター リストには、このデプロイに必要な最小パラメーターが用意されています。
./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.nameOUTPUT 変数を使用すると、プロビジョニングされたクラウド リソースをローカル開発で使用できます。
Azure にアプリケーションをデプロイする
このアプリケーションは、Azure Container Apps を使用して Azure にデプロイできます。
プロジェクトのルートにあるターミナルで、Azure Developer CLI に対して認証を行います。
azd auth loginAzure CLIを使用して既にサインインしている場合は、代わりにこれらの資格情報を使用するように Azure Developer CLI を構成できます。
azd config set auth.useAzCliAuth trueAzure Developer CLI を使用して Azure Container Apps にデプロイします。
azd up次のプロンプトには提供された答えで回答してください。
- 一意の環境名を入力します。
tsp-server-js - 使用する Azure サブスクリプションを選択する: サブスクリプションを選択する
- 使用する Azure の場所を選択する: 近くの場所を選択します
- 使用するリソース グループを選択する: [新しいリソース グループの作成] を選択します
- 新しいリソース グループの名前を入力し、提供されたデフォルトを受け入れてください。
ヒント
無人展開の場合は、
azd upとプロンプトの応答を次のコマンドに置き換えてください。 CI では、まず、azd auth loginリファレンスで説明されているサポートされている非インターアクティブメソッドを使用して認証します。azd env new tsp-server-js --no-prompt azd env set AZURE_SUBSCRIPTION_ID <subscription-id> azd env set AZURE_LOCATION <location> azd up --no-prompt- 一意の環境名を入力します。
デプロイが完了するまで待ちます。 応答には、次のような情報が含まれます。
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.
ブラウザーでアプリケーションを使用する
デプロイ後、次のことができます。
- コンソールで、
EndpointURL を選択してブラウザーで開きます。 - Swagger UI を使用するには、エンドポイントにルート
/.api-docsを追加します。 - API を使用してウィジェットを作成、読み取り、更新、削除するには、各メソッドで 今すぐ試 す機能を使用します。
アプリケーションを拡張する
エンドツーエンドのプロセス全体が機能したら、引き続き API をビルドします。
-
typeSpec 言語の詳細を確認し、
./main.tspに API と API レイヤーの機能を追加します。 -
エミッタを追加し、
./tspconfig.yamlでそのパラメータを設定します。 - TypeSpec ファイルにさらに機能を追加する場合は、サーバー プロジェクトのソース コードでそれらの変更をサポートします。
- Azure ID で パスワードレス認証 を引き続き使用します。
リソースをクリーンアップする
このクイック スタートが完了したら、Azure リソースを削除できます。
azd down
または、Azure portal から直接リソース グループを削除します。
Troubleshooting
ポート 3000 は既に使用されています
エラー: listen EADDRINUSE: address already in use :::3000
Solution:
- ポート 3000 (
netstat -tulpn | grep 3000(macOS/Linux) またはnetstat -ano | findstr :3000(Windows) を使用してプロセスを識別します。 - そのプロセスを停止するか、PORT 環境変数 (
PORT=3001 npm start) を変更します。
Node.js バージョンの不一致
Error:tsp: command not found または npm ERR! The engine "node" is incompatible
Solution:
- Node.js のバージョン (
node --version) を確認します。 - LTS バージョンであることを確認します。 nodejs.org またはパッケージ マネージャーから更新します。
- npm キャッシュをクリアする:
npm cache clean --forceしてから、再インストールします:npm install -g @typespec/compiler。
@typespec/http-server-js エミッタが見つかりません
エラー: Error: Cannot find module '@typespec/http-server-js'
Solution:
- TypeSpec プロジェクト ルートから、エミッターをインストールします:
npm install @typespec/http-server-js。 - エミッタが
tspconfig.yamlの下のemit:に一覧表示されていることを確認します。 - 再コンパイル:
tsp compile .。
生成されたサーバーでの TypeScript コンパイル エラー
Error:error TS2322: Type 'X' is not assignable to type 'Y' 生成されたファイル内
Solution:
-
main.tsp、tsp compile .、そしてcd ./tsp-output/server && tscを変更した後、サーバーを再生成します。 - カスタム コントローラー コードが ディレクトリ
./generatedにあることを確認します。 生成されたファイルを直接編集しないでください。
起動時に Express.js サーバーがクラッシュする
Error:listen EADDRINUSE または Error: Cannot find module '@azure/cosmos'
Solution:
- 不足している依存関係をインストールする:
cd ./tsp-output/server && npm install @azure/cosmos @azure/identity。 - ポートの競合の場合は、
index.tsで PORT を変更するか、PORT=3001 npm startを使用します。