Compartir a través de


Inicio rápido: Creación de un nuevo proyecto de API con TypeSpec y TypeScript

En este inicio rápido: aprenda a usar TypeSpec para diseñar, generar e implementar una aplicación de API de TypeScript RESTful. TypeSpec es un lenguaje de código abierto para describir las API de servicio en la nube y genera código de cliente y servidor para varias plataformas. Al seguir este inicio rápido, aprenderá a definir el contrato de API una vez y a generar implementaciones coherentes, lo que le ayudará a crear servicios de API más fáciles de mantener y bien documentados.

En esta guía de inicio rápido:

  • Definición de la API mediante TypeSpec
  • Creación de una aplicación de servidor de API
  • Integración de Azure Cosmos DB para el almacenamiento persistente
  • Implementación en Azure
  • Ejecución y prueba de la API

Important

@typespec/http-server-js el emisor está en este momento en versión preliminar. Esta información está relacionada con un producto de versión preliminar que puede modificarse sustancialmente antes de su lanzamiento. Microsoft no ofrece ninguna garantía, expresada o implícita, con respecto a la información proporcionada aquí.

Prerequisites

Desarrollo con TypeSpec

TypeSpec define la API de forma independiente del lenguaje y genera el servidor de API y la biblioteca cliente para varias plataformas. Esta funcionalidad le permite:

  • Define tu contrato de API una vez
  • Generación de código de cliente y servidor coherentes
  • Centrarse en la implementación de lógica de negocios en lugar de en la infraestructura de API

TypeSpec proporciona administración de servicios de API:

  • Lenguaje de definición de API
  • Middleware de enrutamiento del lado servidor para API
  • Bibliotecas cliente para consumir API

Proporcione solicitudes de cliente e integraciones de servidor:

  • Implementación de lógica de negocios en middleware, como servicios de Azure para bases de datos, almacenamiento y mensajería
  • Servidor de hospedaje para la API (local o en Azure)
  • Scripts de implementación para el aprovisionamiento y despliegue repetibles

Creación de una nueva aplicación TypeSpec

  1. Cree una carpeta para almacenar el servidor de API y los archivos TypeSpec.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Instale el compilador TypeSpec globalmente:

    npm install -g @typespec/compiler
    
  3. Compruebe TypeSpec instalado correctamente:

    tsp --version
    
  4. Inicialice el proyecto TypeSpec:

    tsp init
    
  5. Responda a las siguientes preguntas con las respuestas proporcionadas.

    • ¿Inicializa un nuevo proyecto aquí? Y
    • ¿Selecciona una plantilla de proyecto? API REST genérica
    • Escriba un nombre de proyecto: Widgets
    • ¿Qué emisores desea usar?
      • Documento de OpenAPI 3.1
      • Códigos auxiliares del servidor JavaScript

    Los emisores de TypeSpec son bibliotecas que utilizan varias API del compilador de TypeSpec para reflejar el proceso de compilación y generar artefactos.

  6. Espere a que se complete la inicialización antes de continuar.

  7. Compile el proyecto:

    tsp compile .
    
  8. TypeSpec genera el proyecto predeterminado en ./tsp-output, creando dos carpetas independientes:

    • schema es la especificación OpenApi 3. Tenga en cuenta que las pocas líneas en ./main.tsp generaron más de 200 líneas de la especificación de OpenApi para usted.
    • server es el middleware generado. Este middleware se puede incorporar en un proyecto de servidor Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts define las interfaces de la API de widgets.
      • ./tsp-output/js/src/generated/http/openapi3.ts define la especificación de Open API como un archivo TypeScript y se vuelve a generar cada vez que compile el proyecto TypeSpec.

Configuración de emisores typeSpec

Emplea los archivos TypeSpec para configurar la generación del servidor de API y estructurar completamente el servidor Express.js.

  1. ./tsconfig.yaml Abra y reemplace la configuración existente por el código YAML siguiente:

    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
    

    Esta configuración crea un servidor de API de Express.js completo:

    • express: genere el servidor de API de Express.js, incluida la interfaz de usuario de Swagger.
    • emitter-output-dir: genere todo en el directorio ./server.
  2. Elimine el existente ./tsp-output. No se preocupe, generará el servidor en el paso siguiente.

  3. Use el emisor de JavaScript TypeSpec para crear el servidor Express.js:

    npx hsjs-scaffold
    
  4. Cambie al nuevo ./tsp-output/server directorio:

    cd ./tsp-output/server
    
  5. Compile TypeScript en JavaScript.

    tsc
    
  6. Ejecute el proyecto:

    npm start
    

    Espere a que la notificación se abra en el explorador.

  7. Abra el explorador y vaya a http://localhost:3000/.api-docs.

    Captura de pantalla del explorador que muestra la interfaz de usuario de Swagger para widgets API.

  8. La API TypeSpec predeterminada y el servidor funcionan. Si desea finalizar este servidor de API, agregue la lógica empresarial para admitir las APIs de Widgets en ./tsp-output/server/src/controllers/widgets.ts. La interfaz de usuario está conectada a la API que devuelve datos falsos codificados de forma dura.

Descripción de la estructura de archivos de aplicación

La estructura del proyecto de Express.js que se encuentra en tsp-output/server/ incluye el servidor generado, el package.jsony el middleware para la integración de 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 estructura de archivos del proyecto TypeSpec primario incluye este proyecto de Express.js en tsp-output:

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

Cambio de la persistencia a Azure Cosmos DB no-sql

Ahora que el servidor básico de API de Express.js funciona, actualice el servidor Express.js para trabajar con Azure Cosmos DB para un almacén de datos persistente. Esto incluye cambios para usar la integración de Cosmos DB en el index.ts middleware. Todos los cambios deben producirse fuera del ./tsp-output/server/src/generated directorio.

  1. En el ./tsp-output/server directorio , agregue Azure Cosmos DB al proyecto:

    npm install @azure/cosmos
    
  2. Agregue la biblioteca de identidades de Azure para autenticarse en Azure:

    npm install @azure/identity
    
  3. Cree un ./tsp-output/server/src/azure directorio para contener código fuente específico de Azure.

  4. Cree el cosmosClient.ts archivo en ese directorio para crear un objeto de cliente de Cosmos DB y pegue el código siguiente:

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

    Observe que el archivo usa el punto de conexión, la base de datos y el contenedor. No necesita una cadena de conexión ni una clave porque usa la credencial DefaultAzureCredentialde identidad de Azure. Obtenga más información sobre este método de autenticación segura para entornos locales y de producción .

  5. Cree un nuevo controlador de widget, ./tsp-output/server/src/controllers/WidgetsCosmos.tsy pegue el código de integración siguiente para 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. Actualice el ./tsp-output/server/src/index.ts para importar el nuevo controlador, obtenga la configuración del entorno de Azure Cosmos DB, y luego, cree el WidgetsCosmosController y páselo al enrutador.

    // 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. En un terminal de ./tsp-output/server, compile TypeScript en JavaScript.

    tsc
    

    El proyecto ahora se desarrolla con la integración de Cosmos DB. Vamos a crear los scripts de implementación para crear los recursos de Azure e implementar el proyecto.

Creación de una infraestructura de implementación

Cree los archivos necesarios para tener una implementación repetible con la CLI para desarrolladores de Azure y las plantillas de Bicep.

  1. En la raíz del proyecto TypeSpec, cree un azure.yaml archivo de definición de implementación y pegue el siguiente código:

    # 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
    

    Observe que esta configuración hace referencia a todo el proyecto TypeSpec.

  2. En la raíz del proyecto TypeSpec, cree el ./Dockerfile que se usa para compilar el contenedor para 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. En la raíz del proyecto TypeSpec, cree un ./infra directorio.

  4. Cree un ./infra/main.bicepparam archivo y copie lo siguiente para definir los parámetros que necesitamos para la implementación:

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

    Esta lista de parámetros proporciona los parámetros mínimos necesarios para esta implementación.

  5. Cree un ./infra/main.bicep archivo y copie lo siguiente para definir los recursos de Azure para el aprovisionamiento y la implementación:

    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
    

    Las variables OUTPUT permiten usar los recursos en la nube aprovisionados con el desarrollo local.

Implementación de una aplicación en Azure

Puede implementar esta aplicación en Azure mediante Azure Container Apps:

  1. En un terminal en la raíz del proyecto, autentíquese en la CLI para desarrolladores de Azure:

    azd auth login
    
  2. Implemente en Azure Container Apps mediante la CLI para desarrolladores de Azure:

    azd up
    
  3. Responda a las siguientes preguntas con las respuestas proporcionadas.

    • Escriba un nombre de entorno único: tsp-server-js
    • Selección de una suscripción de Azure que se va a usar: seleccione la suscripción.
    • Selección de una ubicación de Azure que se va a usar: seleccione una ubicación cercana.
    • Selección de un grupo de recursos que se va a usar: seleccione Crear un nuevo grupo de recursos.
    • Escriba un nombre para el nuevo grupo de recursos: acepte el valor predeterminado proporcionado.
  4. Espere hasta que se complete la implementación. La respuesta incluye información similar a la siguiente:

    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.
    

Uso de la aplicación en el explorador

Una vez implementado, puede hacer lo siguiente:

  1. En la consola, seleccione la Endpoint dirección URL para abrirla en un explorador.
  2. Agregue la ruta, /.api-docs, al punto de conexión para usar la interfaz de usuario de Swagger.
  3. Use la característica Pruébelo ahora en cada método para crear, leer, actualizar y eliminar widgets a través de la API.

Desarrollar tu aplicación

Ahora que ha funcionado todo el proceso de un extremo a otro, siga compilando la API:

  • Obtenga más información sobre el lenguaje TypeSpec para agregar más API y características de capa de API en ./main.tsp.
  • Agregue más emisores y configure sus parámetros en ../tspconfig.yaml
  • A medida que agregue más características en los archivos TypeSpec, admita esos cambios con código fuente en el proyecto de servidor.
  • Siga usando la autenticación sin contraseña con Azure Identity.

Limpieza de recursos

Cuando haya terminado con este inicio rápido, puede quitar los recursos de Azure:

azd down

O elimine el grupo de recursos directamente desde Azure Portal.

Pasos siguientes