Sdílet prostřednictvím


Rychlý start: Vytvoření nového projektu rozhraní API pomocí TypeSpec a TypeScriptu

V tomto rychlém startu: Naučte se používat TypeSpec k návrhu, generování a implementaci aplikace RESTful TypeScript API. TypeSpec je opensourcový jazyk pro popis rozhraní API cloudové služby a generuje kód klienta a serveru pro více platforem. V tomto rychlém startu se dozvíte, jak definovat kontrakt rozhraní API jednou a generovat konzistentní implementace, což vám pomůže vytvářet lépe udržovatelné a dobře zdokumentované služby rozhraní API.

V tomto rychlém průvodci vám:

  • Definování rozhraní API pomocí TypeSpec
  • Vytvoření serverové aplikace rozhraní API
  • Integrace služby Azure Cosmos DB pro trvalé úložiště
  • Nasazení do Azure
  • Spuštění a otestování rozhraní API

Important

@typespec/http-server-js emitter je aktuálně ve verzi PREVIEW. Tyto informace se týkají předběžné verze produktu, který může být podstatně změněn před vydáním. Společnost Microsoft neposkytuje žádné záruky, vyjádřené ani předpokládané, pokud jde o informace uvedené zde.

Prerequisites

Vývoj pomocí TypeSpec

TypeSpec definuje vaše rozhraní API nezávislou na jazyce a vygeneruje server rozhraní API a klientskou knihovnu pro více platforem. Tato funkce umožňuje:

  • Definujte kontrakt rozhraní API jenom jednou
  • Generování konzistentního serveru a klientského kódu
  • Zaměřte se na implementaci obchodní logiky místo infrastruktury rozhraní API.

TypeSpec poskytuje správu služeb pro API:

  • Jazyk definice rozhraní API
  • Middleware pro směrování na serverové straně pro rozhraní API
  • Klientské knihovny pro využívání rozhraní API

Poskytujete požadavky klientů a integraci serverů:

  • Implementace obchodní logiky v middlewaru, jako jsou služby Azure pro databáze, úložiště a zasílání zpráv
  • Hostování serveru pro vaše rozhraní API (místně nebo v Azure)
  • Skripty nasazení pro opakovatelné zřizování a nasazení

Vytvoření nové aplikace TypeSpec

  1. Vytvořte novou složku pro uložení serveru rozhraní API a souborů TypeSpec.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Globální instalace kompilátoru TypeSpec :

    npm install -g @typespec/compiler
    
  3. Zkontrolujte, jestli je TypeSpec správně nainstalovaný:

    tsp --version
    
  4. Inicializace projektu TypeSpec:

    tsp init
    
  5. Odpovězte na následující otázky pomocí poskytnutých odpovědí.

    • Inicializujete tady nový projekt? Y
    • Vyberte šablonu projektu? Obecné rozhraní REST API
    • Zadejte název projektu: Widgety
    • Jaké emitory chcete použít?
      • Dokument OpenAPI 3.1
      • Stuby serveru JavaScriptu

    Emitory TypeSpec jsou knihovny, které využívají různá rozhraní API kompilátoru TypeSpec pro reflektování procesu kompilace TypeSpec a generování artefaktů.

  6. Než budete pokračovat, počkejte na dokončení inicializace.

  7. Zkompilujte projekt:

    tsp compile .
    
  8. TypeSpec vygeneruje výchozí projekt v ./tsp-output, vytvoření dvou samostatných složek:

    • schéma je specifikace OpenApi 3. Všimněte si, že několik řádků v ./main.tsp vygenerovalo více než 200 řádků specifikace OpenApi za vás.
    • Vygenerovaný middleware je server. Tento middleware lze začlenit do projektu serveru Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts definuje rozhraní pro rozhraní API widgetů.
      • ./tsp-output/js/src/generated/http/openapi3.ts definuje specifikaci rozhraní Open API jako soubor TypeScript a znovu se vygeneruje při každém kompilaci projektu TypeSpec.

Konfigurace emitorů TypeSpec

Pomocí souborů TypeSpec nakonfigurujte generování API serveru tak, aby se vygeneroval celý Express.js server jako základ.

  1. Otevřete ./tsconfig.yaml a nahraďte stávající konfiguraci následujícím 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
    

    Tato konfigurace vytvoří úplný Express.js server rozhraní API:

    • express: Vygenerujte server rozhraní API Express.js, včetně uživatelského rozhraní Swagger.
    • emitter-output-dir: Vygenerujte vše do ./server adresáře.
  2. Odstranit existující ./tsp-output. Nedělejte si starosti, server vygenerujete v dalším kroku.

  3. K vytvoření Express.js serveru použijte emitor TypeSpec JavaScript:

    npx hsjs-scaffold
    
  4. Přejděte do nového ./tsp-output/server adresáře:

    cd ./tsp-output/server
    
  5. Zkompilujte TypeScript do JavaScriptu.

    tsc
    
  6. Spusťte projekt:

    npm start
    

    Počkejte, až se oznámení otevře v prohlížeči.

  7. Otevřete prohlížeč a přejděte na http://localhost:3000/.api-docs.

    Snímek obrazovky prohlížeče s uživatelským rozhraním Swagger pro rozhraní API widgetů

  8. Výchozí rozhraní API TypeSpec i server fungují. Pokud chcete dokončit tento API server, přidejte obchodní logiku, která podpoří rozhraní API widgetů v ./tsp-output/server/src/controllers/widgets.ts. Uživatelské rozhraní je připojené k rozhraní API, které vrací pevně zakódovaná falešná data.

Principy struktury souborů aplikace

Struktura projektu Express.js, nalezená na tsp-output/server/, zahrnuje vygenerovaný server, soubor package.json a middleware pro vaši integraci s 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 souborů nadřazeného projektu TypeSpec zahrnuje tento Express.js projekt v tsp-output:

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

Změna trvalosti na Azure Cosmos DB no-sql

Teď, když základní server rozhraní API Express.js funguje, aktualizujte server Express.js tak, aby fungoval se službou Azure Cosmos DB pro trvalé úložiště dat. To zahrnuje změny použití index.ts pro integraci Cosmos DB v middleware. Všechny změny by se měly provést mimo ./tsp-output/server/src/generated adresář.

  1. ./tsp-output/server V adresáři přidejte do projektu Službu Azure Cosmos DB:

    npm install @azure/cosmos
    
  2. Přidejte knihovnu Identit Azure pro ověření do Azure:

    npm install @azure/identity
    
  3. Vytvořte adresář pro uložení zdrojového ./tsp-output/server/src/azure kódu specifického pro Azure.

  4. cosmosClient.ts V tomto adresáři vytvořte soubor, který vytvoří objekt klienta Cosmos DB a vloží následující kód:

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

    Všimněte si, že soubor používá koncový bod, databázi a kontejner. Nepotřebuje připojovací řetězec ani klíč, protože používá přihlašovací údaje Azure DefaultAzureCredential. Přečtěte si další informace o této metodě zabezpečeného ověřování pro místní i produkční prostředí.

  5. Vytvořte nový kontroler ./tsp-output/server/src/controllers/WidgetsCosmos.tswidgetu a vložte následující kód integrace pro službu 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. Aktualizujte ./tsp-output/server/src/index.ts, abyste importovali nový kontroler, získejte nastavení prostředí pro Azure Cosmos DB, a následně vytvořte WidgetsCosmosController a předejte ho směrovači.

    // 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. V terminálu ./tsp-output/serverzkompilujte TypeScript do JavaScriptu.

    tsc
    

    Projekt se teď kompiluje s integrací Cosmos DB. Vytvoříme skripty nasazení, které vytvoří prostředky Azure a nasadí projekt.

Vytvoření infrastruktury nasazení

Vytvořte soubory potřebné k opakovatelnému nasazení pomocí Azure Developer CLI a šablon Bicep.

  1. V kořenovém adresáři projektu TypeSpec vytvořte azure.yaml definiční soubor nasazení a vložte následující zdroj:

    # 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
    

    Všimněte si, že tato konfigurace odkazuje na celý projekt TypeSpec.

  2. V kořenovém adresáři projektu TypeSpec vytvořte ./Dockerfile kontejner, který se používá k sestavení kontejneru pro 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. V kořenovém adresáři projektu TypeSpec vytvořte ./infra adresář.

  4. ./infra/main.bicepparam Vytvořte soubor a zkopírujte ho následujícím způsobem, abyste definovali parametry, které potřebujeme pro nasazení:

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

    Tento seznam parametrů poskytuje minimální parametry potřebné pro toto nasazení.

  5. ./infra/main.bicep Vytvořte soubor a zkopírujte ho následujícím způsobem, abyste definovali prostředky Azure pro zřizování a nasazení:

    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
    

    Proměnné OUTPUT umožňují používat zřízené cloudové prostředky s místním vývojem.

Nasazení aplikace do Azure

Tuto aplikaci můžete nasadit do Azure pomocí Azure Container Apps:

  1. V terminálu v kořenovém adresáři projektu se ověřte v Azure Developer CLI:

    azd auth login
    
  2. Nasazení do Azure Container Apps pomocí Azure Developer CLI:

    azd up
    
  3. Odpovězte na následující otázky pomocí poskytnutých odpovědí.

    • Zadejte jedinečný název prostředí: tsp-server-js
    • Vyberte předplatné Azure, které chcete použít: vyberte své předplatné.
    • Vyberte umístění Azure, které se má použít: vyberte umístění blízko vás.
    • Vyberte skupinu prostředků, která se má použít: Vyberte Vytvořit novou skupinu prostředků.
    • Zadejte název nové skupiny prostředků: Přijměte výchozí zadaný název.
  4. Počkejte, až se nasazení dokončí. Odpověď obsahuje podobné informace:

    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.
    

Použití aplikace v prohlížeči

Po nasazení můžete:

  1. V konzole vyberte Endpoint adresu URL a otevřete ji v prohlížeči.
  2. Přidejte trasu /.api-docsdo koncového bodu pro použití uživatelského rozhraní Swaggeru.
  3. Pomocí funkce Try it now (Vyzkoušet) v jednotlivých metodách můžete vytvářet, číst, aktualizovat a odstraňovat widgety prostřednictvím rozhraní API.

Rozšiřte svou aplikaci

Teď, když máte celý kompletní proces, pokračujte vytvořením rozhraní API:

  • Další informace o jazyku TypeSpec pro přidání dalších rozhraní API a funkcí vrstvy rozhraní API v nástroji ./main.tsp.
  • Přidejte další emitory a nakonfigurujte jejich parametry v souboru ./tspconfig.yaml.
  • Když do souborů TypeSpec přidáte další funkce, podpořte tyto změny zdrojovým kódem v serverovém projektu.
  • Pokračujte v používání ověřování bez hesla s využitím identity Azure.

Vyčistěte zdroje

Po dokončení tohoto rychlého startu můžete odebrat prostředky Azure:

azd down

Nebo skupinu prostředků odstraňte přímo z Azure portalu.

Další kroky