Udostępnij za pomocą


Szybki start: tworzenie nowego projektu interfejsu API przy użyciu klasy TypeSpec i TypeScript

W tym szybkim przewodniku dowiedz się, jak używać TypeSpec do projektowania, generowania i implementowania aplikacji API RESTful TypeScript. TypeSpec to język typu open source służący do opisywania interfejsów API usługi w chmurze i generowania kodu klienta i serwera dla wielu platform. Korzystając z tego przewodnika Szybki start, dowiesz się, jak zdefiniować kontrakt interfejsu API raz i wygenerować spójne implementacje, pomagając tworzyć bardziej konserwowalne i dobrze udokumentowane usługi interfejsu API.

W ramach tego szybkiego przewodnika wykonasz następujące czynności:

  • Definiowanie interfejsu API przy użyciu elementu TypeSpec
  • Tworzenie aplikacji serwera interfejsu API
  • Integracja usługi Azure Cosmos DB do magazynowania trwałego
  • Wdrażanie na platformie Azure
  • Uruchamianie i testowanie interfejsu API

Important

@typespec/http-server-js Emiter jest obecnie w wersji PRÓBNEJ. Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany przed jego wydaniem. Firma Microsoft nie udziela żadnych gwarancji, wyrażonych ani domniemanych, w odniesieniu do podanych tutaj informacji.

Prerequisites

Programowanie przy użyciu klasy TypeSpec

TypeSpec definiuje interfejs API w sposób niezależny od języka i generuje serwer interfejsu API i bibliotekę klienta dla wielu platform. Ta funkcja umożliwia:

  • Zdefiniuj umowę API raz
  • Generowanie spójnego kodu serwera i klienta
  • Skoncentrowanie się na implementowaniu logiki biznesowej, a nie infrastruktury interfejsu API

TypeSpec zapewnia zarządzanie usługami interfejsu API:

  • Język definicji interfejsu API
  • Oprogramowanie pośredniczące routingu po stronie serwera dla interfejsu API
  • Biblioteki klienckie do korzystania z interfejsu API

Udostępniasz żądania klienta i integracje serwerów:

  • Implementowanie logiki biznesowej w warstwie pośredniczącej, takiej jak usługi platformy Azure dla baz danych, przechowywania danych i komunikacji
  • Serwer hostingu dla interfejsu API (lokalnie lub na platformie Azure)
  • Skrypty wdrażania na potrzeby powtarzalnej aprowizacji i wdrażania

Tworzenie nowej aplikacji TypeSpec

  1. Utwórz nowy folder do przechowywania serwera interfejsu API i plików TypeSpec.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Zainstaluj kompilator TypeSpec globalnie:

    npm install -g @typespec/compiler
    
  3. Sprawdź, czy klasa TypeSpec jest zainstalowana poprawnie:

    tsp --version
    
  4. Zainicjuj projekt TypeSpec:

    tsp init
    
  5. Odpowiedz na następujące monity, odpowiadając na podane odpowiedzi:

    • Zainicjuj nowy projekt tutaj? Y
    • Wybierz szablon projektu? Ogólny interfejs API REST
    • Wprowadź nazwę projektu: Widżety
    • Jakich emiterów chcesz użyć?
      • Dokument OpenAPI 3.1
      • Szkielety serwera JavaScript

    Emitery TypeSpec to biblioteki, które korzystają z różnych interfejsów API kompilatora TypeSpec, aby odzwierciedlić proces kompilacji TypeSpec i generować artefakty.

  6. Przed kontynuowaniem poczekaj na ukończenie inicjowania.

  7. Skompiluj projekt:

    tsp compile .
    
  8. TypeSpec generuje projekt domyślny w ./tsp-output, tworząc dwa oddzielne foldery.

    • schemat jest specyfikacją OpenAPI 3. Zwróć uwagę, że kilka linii w ./main.tsp wygenerowało ponad 200 wierszy specyfikacji OpenApi.
    • serwer to wygenerowane oprogramowanie pośredniczące. To oprogramowanie pośredniczące można włączyć do projektu serwera Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts definiuje interfejsy API widżetów.
      • ./tsp-output/js/src/generated/http/openapi3.ts definiuje specyfikację OpenAPI jako plik TypeScript, który jest ponownie generowany za każdym razem, gdy kompilujesz projekt TypeSpec.

Konfigurowanie modułów emiterów TypeSpec

Użyj plików TypeSpec, aby skonfigurować generowanie serwera API do kreacji struktury całego serwera Express.js.

  1. Otwórz plik ./tsconfig.yaml i zastąp istniejącą konfigurację następującym kodem 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
    

    Ta konfiguracja tworzy kompletny serwer interfejsu API Express.js:

    • express: Wygeneruj serwer interfejsu API Express.js, w tym interfejs Swagger UI.
    • emitter-output-dir: Generuj wszystko do ./server katalogu.
  2. Usuń istniejący element ./tsp-output. Nie martw się, w następnym kroku wygenerujesz serwer.

  3. Użyj emitera TypeSpec JavaScript, aby utworzyć serwer Express.js:

    npx hsjs-scaffold
    
  4. Przejdź do nowego ./tsp-output/server katalogu:

    cd ./tsp-output/server
    
  5. Skompiluj kod TypeScript do języka JavaScript.

    tsc
    
  6. Uruchom projekt:

    npm start
    

    Poczekaj na otwarcie powiadomienia w przeglądarce.

  7. Otwórz przeglądarkę i przejdź do http://localhost:3000/.api-docs.

    Zrzut ekranu przeglądarki z interfejsem użytkownika Swagger dla API widżetów.

  8. Domyślny interfejs API TypeSpec i serwer działają. Jeśli chcesz dokończyć ten serwer API, dodaj logikę biznesową, aby obsługiwał API widżetów w ./tsp-output/server/src/controllers/widgets.ts. Interfejs użytkownika jest połączony z interfejsem API, który zwraca zakodowane na stałe fałszywe dane.

Omówienie struktury plików aplikacji

Struktura projektu Express.js znaleziona pod adresem tsp-output/server/ zawiera wygenerowany serwer, package.json i middleware dla integracji z 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

Struktura plików nadrzędnego projektu TypeSpec obejmuje ten projekt Express.js w pliku :tsp-output

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

Zmień utrzymywanie stanu na Azure Cosmos DB NoSQL

Teraz, gdy podstawowy serwer interfejsu API Express.js działa, zaktualizuj serwer Express.js do pracy z usługą Azure Cosmos DB w celu uzyskania trwałego magazynu danych. Obejmuje to zmiany w index.ts w celu korzystania z integracji z Cosmos DB w pośredniku. Wszystkie zmiany powinny nastąpić poza katalogem ./tsp-output/server/src/generated .

  1. W katalogu ./tsp-output/server dodaj Azure Cosmos DB do projektu:

    npm install @azure/cosmos
    
  2. Dodaj bibliotekę tożsamości platformy Azure , aby uwierzytelnić się na platformie Azure:

    npm install @azure/identity
    
  3. Utwórz katalog do przechowywania kodu źródłowego ./tsp-output/server/src/azure specyficznego dla platformy Azure.

  4. cosmosClient.ts Utwórz plik w tym katalogu, aby utworzyć obiekt klienta usługi Cosmos DB i wkleić następujący kod:

    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'}`
      };
    };
    

    Zwróć uwagę, że plik używa punktu końcowego, bazy danych i kontenera. Nie wymaga parametrów połączenia ani klucza, ponieważ używa poświadczeń DefaultAzureCredentialtożsamości platformy Azure. Dowiedz się więcej o tej metodzie bezpiecznego uwierzytelniania w środowiskach lokalnych i produkcyjnych .

  5. Utwórz nowy kontroler widżetu i ./tsp-output/server/src/controllers/WidgetsCosmos.tswklej następujący kod integracji dla usługi 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. Zaktualizuj ./tsp-output/server/src/index.ts, aby zaimportować nowy kontroler, pobierz ustawienia środowiska Azure Cosmos DB, a następnie utwórz WidgetsCosmosController i przekaż go do routera.

    // 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. W terminalu w witrynie ./tsp-output/serverskompiluj kod TypeScript do języka JavaScript.

    tsc
    

    Projekt jest teraz kompiluje przy użyciu integracji z usługą Cosmos DB. Utwórzmy skrypty wdrażania, aby utworzyć zasoby platformy Azure i wdrożyć projekt.

Tworzenie infrastruktury wdrażania

Utwórz pliki potrzebne do powtarzalnego wdrożenia przy użyciu interfejsu wiersza polecenia dla deweloperów platformy Azure i szablonów Bicep.

  1. W katalogu głównym projektu TypeSpec utwórz plik definicji wdrożenia azure.yaml i wklej do niego następujące źródło:

    # 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
    

    Zwróć uwagę, że ta konfiguracja odwołuje się do całego projektu TypeSpec.

  2. W katalogu głównym projektu TypeSpec utwórz ./Dockerfile, który jest używany do budowy kontenera dla usługi 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. W katalogu głównym projektu TypeSpec utwórz katalog ./infra.

  4. ./infra/main.bicepparam Utwórz plik i skopiuj w następujący sposób, aby zdefiniować parametry potrzebne do wdrożenia:

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

    Ta lista parametrów zawiera minimalne parametry wymagane do tego wdrożenia.

  5. ./infra/main.bicep Utwórz plik i skopiuj w następujący sposób, aby zdefiniować zasoby platformy Azure na potrzeby aprowizacji i wdrażania:

    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
    

    Zmienne OUTPUT umożliwiają używanie zaaprowizowanych zasobów w chmurze z lokalnym programowaniem.

Wdrażanie aplikacji na platformie Azure

Tę aplikację można wdrożyć na platformie Azure przy użyciu usługi Azure Container Apps:

  1. W terminalu w katalogu głównym projektu uwierzytelnij się w Azure Developer CLI.

    azd auth login
    
  2. Wdrażanie w usłudze Azure Container Apps przy użyciu interfejsu wiersza polecenia dla deweloperów platformy Azure:

    azd up
    
  3. Odpowiedz na następujące monity, odpowiadając na podane odpowiedzi:

    • Wprowadź unikatową nazwę środowiska: tsp-server-js
    • Wybierz subskrypcję platformy Azure do użycia: wybierz subskrypcję
    • Wybierz lokalizację platformy Azure do użycia: wybierz lokalizację w pobliżu
    • Wybierz grupę zasobów do użycia: wybierz pozycję Utwórz nową grupę zasobów
    • Wprowadź nazwę nowej grupy zasobów: zaakceptuj podaną wartość domyślną
  4. Poczekaj na zakończenie wdrażania. Odpowiedź zawiera informacje podobne do następujących:

    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.
    

Korzystanie z aplikacji w przeglądarce

Po wdrożeniu można wykonywać następujące czynności:

  1. W konsoli wybierz Endpoint adres URL, aby otworzyć go w przeglądarce.
  2. Dodaj trasę /.api-docs do punktu końcowego, aby użyć Swagger UI.
  3. Użyj funkcji Wypróbuj teraz w każdej metodzie, aby tworzyć, odczytywać, aktualizować i usuwać widżety za pośrednictwem interfejsu API.

Zwiększanie aplikacji

Teraz, gdy działa cały proces od początku do końca, kontynuuj rozwijanie swojego interfejsu API.

  • Dowiedz się więcej o języku TypeSpec , aby dodać więcej interfejsów API i funkcji warstwy interfejsu API w pliku ./main.tsp.
  • Dodaj więcej emiterów i skonfiguruj ich parametry w pliku ./tspconfig.yaml.
  • W miarę dodawania dodatkowych funkcji w plikach TypeSpec obsługuj te zmiany za pomocą kodu źródłowego w projekcie serwera.
  • Kontynuuj korzystanie z uwierzytelniania bez hasła za pomocą usługi Azure Identity.

Uprzątnij zasoby

Po zakończeniu pracy z przewodnikiem szybkiego startu możesz usunąć zasoby platformy Azure.

azd down

Możesz też usunąć grupę zasobów bezpośrednio z witryny Azure Portal.

Dalsze kroki