Краткое руководство: Создайте новый проект API с помощью TypeSpec и TypeScript

В этом кратком руководстве описано, как использовать TypeSpec для разработки, создания и реализации приложения API RESTful TypeScript. TypeSpec — это язык с открытым исходным кодом для описания API облачных служб и создания кода клиента и сервера для нескольких платформ. Следуя этому краткому руководству, вы узнаете, как определить контракт API один раз и создать согласованные реализации, помогая создавать более доступные и хорошо документированные службы API.

Вы будете использовать генератор кода TypeSpec для скаффолдинга сервера Express.js с маршрутизацией и Swagger UI, подключать его к Azure Cosmos DB для сохранения данных и развертывать в Контейнеры приложений Azure.

Сведения о роли TypeSpec в разработке API см. в разделе "Обзор TypeSpec".

Время завершения: 15–20 минут

В этом кратком руководстве вы сможете:

  • Определение API с помощью TypeSpec
  • Создание приложения сервера API
  • Интеграция Azure Cosmos DB для постоянного хранилища
  • Развертывание в Azure
  • Запуск и тестирование API

Important

@typespec/http-server-js эмиттер в настоящее время находится в предварительной версии. Эта информация относится к предварительному продукту, который может быть существенно изменен до выпуска. Корпорация Майкрософт не предоставляет никаких гарантий, выраженных или подразумеваемых, в отношении информации, предоставленной здесь.

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 — это библиотеки, использующие различные API компилятора TypeSpec для отражения процесса компиляции 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.

  6. Дождитесь завершения инициализации, прежде чем продолжить.

    Note

    tsp init устанавливает зависимости проекта. Если установка зависимостей завершается сбоем после создания файлов проекта, запустите npm install из корневого каталога проекта TypeSpec перед продолжением.

  7. Скомпилируйте проект:

    tsp compile .
    

    Подсказка

    Для итеративной разработки используйте режим наблюдения для автоматической повторной компиляции изменений в файле: tsp compile . --watch Это обеспечивает актуальность созданного сервера и схемы при изменении main.tsp.

  8. TypeSpec создает проект по умолчанию в ./tsp-output, создавая две отдельные папки:

    • схема — это спецификация OpenApi 3. Обратите внимание, что несколько строк в ./main.tsp создали для вас более 200 строк спецификации OpenApi.
    • сервер — это созданное ПО промежуточного слоя. Это ПО промежуточного слоя можно включить в проект сервера Node.js.
      • ./tsp-output/server/js/src/generated/models/all/demo-service.ts определяет интерфейсы ДЛЯ API мини-приложений.
      • ./tsp-output/server/js/src/generated/http/openapi3.ts определяет спецификацию Open API как файл TypeScript и повторно создается при компиляции проекта TypeSpec.

Настройка эмитаторов TypeSpec

Используйте файлы TypeSpec для настройки генерации API-сервера с целью полного построения сервера Express.js.

  1. Откройте ./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
    

    Эта конфигурация создает полный сервер API Express.js:

    • express: создайте сервер API Express.js, включая пользовательский интерфейс Swagger.
    • emitter-output-dir: сгенерируйте все в ./server каталог.
  2. Удалите существующий ./tsp-output. Не волнуйтесь, вы создадите сервер на следующем шаге.

  3. Используйте JavaScript-эмиттер TypeSpec для создания сервера 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.

    Снимок экрана: браузер, отображающий пользовательский интерфейс Swagger для API мини-приложений.

  8. Api TypeSpec по умолчанию и сервер работают. Если вы хотите завершить работу с этим сервером API, добавьте бизнес-логику для поддержки API мини-приложений в ./tsp-output/server/src/controllers/widgets.ts. Пользовательский интерфейс подключен к API, который возвращает закодированные поддельные данные.

Общие сведения о структуре файлов приложения

Структура проекта Express.js, найденная в tsp-output/server/, включает созданный сервер, 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

Теперь, когда базовый сервер API Express.js работает, обновите сервер Express.js для работы с Azure Cosmos DB для постоянного хранилища данных. Изменения касаются использования интеграции 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. Создайте контроллер виджета ./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;
      }
    }
    
  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 и шаблонов Bicep.

  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.

    # 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"]
    
  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 с помощью приложений контейнеров Azure:

  1. В терминале в корне проекта выполните проверку подлинности в интерфейсе командной строки разработчика Azure:

    azd auth login
    

    Если вы уже выполнили вход с помощью Azure CLI, можно настроить интерфейс командной строки разработчика Azure для использования этих учетных данных:

    azd config set auth.useAzCliAuth true
    
  2. Развертывание в приложениях контейнеров Azure с помощью Интерфейса командной строки разработчика Azure:

    azd up
    
  3. Ответьте на следующие запросы, используя предоставленные ответы.

    • Введите уникальное имя среды: 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
    
  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. Используйте функцию Try it now для каждого метода для создания, чтения, обновления и удаления мини-приложений через API.

Расширение приложения

Теперь, когда у вас есть весь процесс от начала до конца, продолжайте разрабатывать свой API.

  • Узнайте больше о языке TypeSpec, чтобы добавить дополнительные функции и возможности слоя API в ./main.tsp.
  • Добавьте больше излучателей и настройте их параметры в ./tspconfig.yaml.
  • При добавлении новых функций в файлы TypeSpec обеспечьте поддержку этих изменений с помощью исходного кода в серверном проекте.
  • Продолжайте использовать проверку подлинности без пароля с удостоверением Azure.

Очистите ресурсы

Когда вы закончите работу с этим быстрым стартом, вы можете удалить ресурсы Azure.

azd down

Или удалите группу ресурсов непосредственно на портале Azure.

Troubleshooting

Порт 3000 уже используется

Ошибка:listen EADDRINUSE: address already in use :::3000

Solution:

  1. Определите процесс с помощью порта 3000: netstat -tulpn | grep 3000 (macOS/Linux) или netstat -ano | findstr :3000 (Windows).
  2. Остановите этот процесс или измените переменную среды PORT: PORT=3001 npm start

несоответствие версий Node.js

Ошибка:tsp: command not found Или npm ERR! The engine "node" is incompatible

Solution:

  1. Проверьте версию Node.js: node --version
  2. Убедитесь, что это версия LTS. Обновление из nodejs.org или диспетчера пакетов.
  3. Очистка кэша npm: npm cache clean --forceи переустановка: npm install -g @typespec/compiler

@typespec/http-server-js излучатель не найден

Ошибка:Error: Cannot find module '@typespec/http-server-js'

Solution:

  1. В корневом каталоге проекта TypeSpec установите эмитатор: npm install @typespec/http-server-js.
  2. Убедитесь, что эмиттер перечислен в tspconfig.yaml под emit:.
  3. Перекомпилировать: tsp compile ..

Ошибка компиляции TypeScript на созданном сервере

Ошибка:error TS2322: Type 'X' is not assignable to type 'Y' в созданных файлах

Solution:

  1. Повторно создайте сервер после изменения main.tsp: tsp compile ., а затем cd ./tsp-output/server && tsc.
  2. Убедитесь, что код пользовательского контроллера находится за пределами./generated каталога. Не изменяйте созданные файлы напрямую.

Express.js сервер завершает работу при запуске

Ошибка:listen EADDRINUSE Или Error: Cannot find module '@azure/cosmos'

Solution:

  1. Установите отсутствующие зависимости: cd ./tsp-output/server && npm install @azure/cosmos @azure/identity
  2. Для устранения конфликтов портов измените PORT в index.ts или используйте PORT=3001 npm start.

Дальнейшие шаги