Condividi tramite


Guida introduttiva: Creare un nuovo progetto API con TypeSpec e TypeScript

Questa guida introduttiva illustra come usare TypeSpec per progettare, generare e implementare un'applicazione API TypeScript RESTful. TypeSpec è un linguaggio open source per descrivere le API del servizio cloud e genera codice client e server per più piattaforme. Seguendo questa guida introduttiva si apprenderà come definire il contratto API una sola volta e generare implementazioni coerenti, consentendo di creare servizi API più gestibili e ben documentati.

Questa guida introduttiva spiega come:

  • Definire l'API usando TypeSpec
  • Creare un'applicazione server API
  • Integrare Azure Cosmos DB per l'archiviazione permanente
  • Distribuzione su Azure
  • Eseguire e testare l'API

Important

@typespec/http-server-js l'emettitore è attualmente in ANTEPRIMA. Queste informazioni si riferiscono a un prodotto in versione preliminare che può essere modificato in modo sostanziale prima del rilascio. Microsoft non fornisce alcuna garanzia, espressa o implicita, in relazione alle informazioni fornite qui.

Prerequisites

Sviluppo con TypeSpec

TypeSpec definisce l'API in modo indipendente dal linguaggio e genera il server API e la libreria client per più piattaforme. Questa funzionalità consente di:

  • Definire il contratto API una sola volta
  • Generare codice client e server coerenti
  • Concentrarsi sull'implementazione della logica di business anziché sull'infrastruttura API

TypeSpec fornisce la gestione dei servizi API:

  • Linguaggio di definizione API
  • Middleware di routing lato server per l'API
  • Librerie client per l'utilizzo dell'API

Tu fornisci richieste client e integrazioni server:

  • Implementare la logica di business nel middleware, ad esempio i servizi di Azure per database, archiviazione e messaggistica
  • Server di hosting per l'API (in locale o in Azure)
  • Script di implementazione per il provisioning e l'implementazione ripetibili

Creare una nuova applicazione TypeSpec

  1. Creare una nuova cartella per contenere il server API e i file TypeSpec.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Installare il compilatore TypeSpec a livello globale:

    npm install -g @typespec/compiler
    
  3. Controllare TypeSpec installato correttamente:

    tsp --version
    
  4. Inizializzare il progetto TypeSpec:

    tsp init
    
  5. Rispondere alle istruzioni seguenti con le risposte fornite:

    • Inizializzare un nuovo progetto qui? Y
    • Selezionare un modello di progetto? API REST generica
    • Immettere un nome di progetto: Widget
    • Quali emettitori vuoi usare?
      • Documento OpenAPI 3.1
      • Stub del server JavaScript

    Gli emettitori TypeSpec sono librerie che utilizzano varie API del compilatore di TypeSpec per interagire con il processo di compilazione di TypeSpec e generare artefatti.

  6. Attendere il completamento dell'inizializzazione prima di continuare.

  7. Compilare il progetto:

    tsp compile .
    
  8. TypeSpec genera il progetto predefinito in ./tsp-output, creando due cartelle separate:

    • schema è la specifica OpenApi 3. Notate che le poche righe in ./main.tsp hanno generato per voi oltre 200 righe della specifica OpenApi.
    • server è il middleware generato. Questo middleware può essere incorporato in un progetto server Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts definisce le interfacce per l'API Widget.
      • ./tsp-output/js/src/generated/http/openapi3.ts definisce la specifica open API come file TypeScript e viene rigenerata ogni volta che si compila il progetto TypeSpec.

Configurare gli emettitori TypeSpec

Usare i file TypeSpec per configurare la generazione del server API e creare la struttura dell'intero server Express.js.

  1. ./tsconfig.yaml Aprire e sostituire la configurazione esistente con il codice YAML seguente:

    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
    

    Questa configurazione crea un server API completo Express.js:

    • express: genera il server API Express.js, inclusa l'interfaccia utente di Swagger.
    • emitter-output-dir: genera tutto nella ./server directory.
  2. Eliminare l'esistente ./tsp-output. Non preoccuparti, il server verrà generato nel passaggio successivo.

  3. Usare l'emettitore JavaScript TypeSpec per creare il server di Express.js:

    npx hsjs-scaffold
    
  4. Passare alla nuova ./tsp-output/server directory:

    cd ./tsp-output/server
    
  5. Compila TypeScript in JavaScript.

    tsc
    
  6. Eseguire il progetto:

    npm start
    

    Attendere che la notifica venga aperta nel browser.

  7. Aprire il browser e passare a http://localhost:3000/.api-docs.

    Screenshot del browser che mostra l'interfaccia utente di Swagger per l'API Widget.

  8. L'API TypeSpec predefinita e il server funzionano entrambi. Se vuoi completare questo server API, aggiungi la logica aziendale per supportare le API Widget in ./tsp-output/server/src/controllers/widgets.ts. L'interfaccia utente è connessa all'API che restituisce dati fittizi predefiniti.

Informazioni sulla struttura dei file dell'applicazione

La struttura del progetto Express.js disponibile in tsp-output/server/ include il server generato, il package.jsone il middleware per l'integrazione di 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

La struttura dei file per il progetto padre TypeSpec include questo progetto Express.js in tsp-output:

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

Modifica la persistenza in Azure Cosmos DB NoSQL

Ora che il server API di base Express.js funziona, aggiornare il server Express.js per l'uso con Azure Cosmos DB per un archivio dati permanente. Sono incluse le modifiche apportate a index.ts per usare l'integrazione di Cosmos DB nel middleware. Tutte le modifiche devono essere apportate all'esterno della ./tsp-output/server/src/generated directory.

  1. ./tsp-output/server Nella directory aggiungere Azure Cosmos DB al progetto:

    npm install @azure/cosmos
    
  2. Aggiungere la libreria di identità di Azure per l'autenticazione in Azure:

    npm install @azure/identity
    
  3. Creare una ./tsp-output/server/src/azure directory per contenere il codice sorgente specifico di Azure.

  4. Creare il cosmosClient.ts file in tale directory per creare un oggetto client Cosmos DB e incollarlo nel codice seguente:

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

    Si noti che il file usa l'endpoint, il database e il contenitore. Non è necessaria una stringa di connessione o una chiave perché usa le credenziali DefaultAzureCredentialdi Identità di Azure. Altre informazioni su questo metodo di autenticazione sicura per ambienti locali e di produzione .

  5. Creare un nuovo controller widget, ./tsp-output/server/src/controllers/WidgetsCosmos.tse incollare il codice di integrazione seguente per 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. Aggiorna il ./tsp-output/server/src/index.ts per importare il nuovo controller, ottenere le impostazioni dell'ambiente Azure Cosmos DB, quindi crea il WidgetsCosmosController e passalo al router.

    // 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. In un terminale in ./tsp-output/servercompilare TypeScript in JavaScript.

    tsc
    

    Ora il progetto viene sviluppato con l'integrazione di Cosmos DB. Creare gli script di distribuzione per creare le risorse di Azure e distribuire il progetto.

Creare un'infrastruttura di distribuzione

Creare i file necessari per una distribuzione ripetibile con Azure Developer CLI e i modelli Bicep.

  1. Alla radice del progetto TypeSpec, crea un azure.yaml file di definizione della distribuzione e incolla il seguente contenuto:

    # 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
    

    Si noti che questa configurazione fa riferimento all'intero progetto TypeSpec.

  2. Alla radice del progetto TypeSpec, creare il ./Dockerfile che viene utilizzato per costruire il contenitore per 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. Nella radice del progetto TypeSpec creare una ./infra directory.

  4. Creare un ./infra/main.bicepparam file e copiarlo nel modo seguente per definire i parametri necessari per la distribuzione:

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

    Questo elenco di parametri fornisce i parametri minimi necessari per questa distribuzione.

  5. Creare un ./infra/main.bicep file e copiarlo nel modo seguente per definire le risorse di Azure per il provisioning e la distribuzione:

    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
    

    Le variabili OUTPUT consentono di usare le risorse cloud di cui è stato effettuato il provisioning con lo sviluppo locale.

Distribuire l'applicazione in Azure

È possibile distribuire questa applicazione in Azure usando App Azure Container:

  1. In un terminale nella radice del progetto eseguire l'autenticazione all'interfaccia della riga di comando per sviluppatori di Azure:

    azd auth login
    
  2. Distribuire su Azure Container Apps usando la CLI per sviluppatori di Azure.

    azd up
    
  3. Rispondere alle istruzioni seguenti con le risposte fornite:

    • Immettere un nome di ambiente univoco: tsp-server-js
    • Selezionare una sottoscrizione di Azure da usare: selezionare la sottoscrizione
    • Selezionare una località di Azure da usare: selezionare una località nelle vicinanze
    • Selezionare un gruppo di risorse da usare: selezionare Crea un nuovo gruppo di risorse
    • Immettere un nome per il nuovo gruppo di risorse: accettare l'impostazione predefinita specificata
  4. Attendere il completamento della distribuzione. La risposta include informazioni simili alle seguenti:

    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.
    

Usare l'applicazione nel browser

Dopo la distribuzione, puoi:

  1. Nella console selezionare l'URL Endpoint per aprirlo in un browser.
  2. Aggiungere la route, /.api-docs, all'endpoint per usare Swagger UI.
  3. Usare la funzionalità Prova ora in ogni metodo per creare, leggere, aggiornare ed eliminare widget tramite l'API.

Espandere l'applicazione

Ora che l'intero processo end-to-end funziona, continuare a compilare l'API:

  • Scopri di più sul linguaggio TypeSpec per aggiungere ulteriori API e funzionalità del livello API in ./main.tsp.
  • Aggiungere altri emettitori e configurare i relativi parametri in ./tspconfig.yaml.
  • Man mano che si aggiungono altre funzionalità nei file TypeSpec, supportare tali modifiche con il codice sorgente nel progetto server.
  • Continuare a usare l'autenticazione senza password con Identità di Azure.

Pulire le risorse

Al termine di questa guida introduttiva, è possibile rimuovere le risorse di Azure:

azd down

In alternativa, eliminare il gruppo di risorse direttamente dal portale di Azure.

Passaggi successivi