Partilhar via


Guia de início rápido: criar um novo projeto de API com TypeSpec e TypeScript

Neste guia de início rápido: saiba como usar o TypeSpec para projetar, gerar e implementar um aplicativo de API RESTful TypeScript. TypeSpec é uma linguagem de código aberto para descrever APIs de serviço de nuvem e gera código de cliente e servidor para várias plataformas. Seguindo este guia de início rápido, você aprende a definir seu contrato de API uma vez e gerar implementações consistentes, ajudando a criar serviços de API mais fáceis de manter e bem documentados.

Neste guia de início rápido, você irá:

  • Defina sua API usando TypeSpec
  • Criar um aplicativo de servidor de API
  • Integrar o Azure Cosmos DB para armazenamento persistente
  • Publicar no Azure
  • Execute e teste a API

Important

@typespec/http-server-js emissor está atualmente em versão de teste. Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Prerequisites

Desenvolvendo com TypeSpec

O TypeSpec define sua API de forma independente de linguagem e gera o servidor de API e a biblioteca de cliente para várias plataformas. Esta funcionalidade permite-lhe:

  • Defina seu contrato de API uma vez
  • Gerar código consistente de servidor e cliente
  • Concentre-se na implementação da lógica de negócios em vez da infraestrutura de API

O TypeSpec fornece gerenciamento de serviços de API:

  • Linguagem de definição da API
  • Middleware de roteamento do lado do servidor para API
  • Bibliotecas de cliente para consumir API

Você fornece solicitações de cliente e integrações de servidor:

  • Implementar lógica de negócios em middleware, como serviços do Azure para bancos de dados, armazenamento e mensagens
  • Servidor de hospedagem para sua API (localmente ou no Azure)
  • Scripts de configuração para aprovisionamento e implantação repetível

Criar um novo aplicativo TypeSpec

  1. Crie uma nova pasta para armazenar o servidor de API e os arquivos TypeSpec.

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

    npm install -g @typespec/compiler
    
  3. Verifique o TypeSpec instalado corretamente:

    tsp --version
    
  4. Inicialize o projeto TypeSpec:

    tsp init
    
  5. Responda às seguintes perguntas usando as respostas fornecidas.

    • Inicializar um novo projeto aqui? Y
    • Selecionar um modelo de projeto? API REST genérica
    • Insira um nome de projeto: Widgets
    • Que emissores pretende utilizar?
      • Documento OpenAPI 3.1
      • Stubs do servidor JavaScript

    Os emissores TypeSpec são bibliotecas que utilizam várias APIs de compilador TypeSpec para refletir sobre o processo de compilação TypeSpec e gerar artefatos.

  6. Aguarde a conclusão da inicialização antes de continuar.

  7. Compile o projeto:

    tsp compile .
    
  8. TypeSpec gera o projeto padrão no ./tsp-output, criando duas pastas separadas:

    • schema é a especificação OpenApi 3. Observe que as poucas linhas em ./main.tsp geraram mais de 200 linhas de especificação OpenApi para você.
    • server é o middleware gerado. Esse middleware pode ser incorporado em um projeto de servidor Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts define as interfaces para a API de widgets.
      • ./tsp-output/js/src/generated/http/openapi3.ts define a especificação Open API como um arquivo TypeScript e é regenerada sempre que você compila seu projeto TypeSpec.

Configurar emissores TypeSpec

Use os arquivos TypeSpec para configurar a geração do servidor de API para organizar todo o servidor Express.js.

  1. Abra o ./tsconfig.yaml e substitua a configuração existente pelo seguinte 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
    

    Esta configuração cria um servidor de API Express.js completo:

    • express: Gere o servidor de API Express.js, incluindo a interface do usuário do Swagger.
    • emitter-output-dir: Gere tudo no ./server diretório.
  2. Excluir o elemento existente ./tsp-output. Não se preocupe, você gerará o servidor na próxima etapa.

  3. Use o emissor JavaScript TypeSpec para criar o servidor Express.js:

    npx hsjs-scaffold
    
  4. Mude para o novo ./tsp-output/server diretório:

    cd ./tsp-output/server
    
  5. Compile o TypeScript em JavaScript.

    tsc
    
  6. Execute o projeto:

    npm start
    

    Aguarde até que a notificação seja aberta no navegador.

  7. Abra o navegador e vá para http://localhost:3000/.api-docs.

    Captura de tela do navegador exibindo Swagger UI for Widgets API.

  8. A API TypeSpec padrão e o servidor funcionam. Se você quiser terminar este servidor de API, adicione sua lógica de negócios para dar suporte às APIs de widgets no ./tsp-output/server/src/controllers/widgets.ts. A interface do usuário está conectada à API que retorna dados falsos codificados.

Compreender a estrutura do arquivo do aplicativo

A estrutura Express.js do projeto encontrada em tsp-output/server/ inclui o servidor gerado, o package.jsone o middleware para sua integração com o 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

A estrutura de arquivo para o projeto pai TypeSpec inclui este projeto Express.js em tsp-output:

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

Alterar a persistência para o Azure Cosmos DB NoSQL

Agora que o servidor de API Express.js básico está funcionando, atualize o servidor Express.js para trabalhar com o Azure Cosmos DB para um armazenamento de dados persistente. Isso inclui alterações na index.ts para utilizar a integração do Cosmos DB no middleware. Todas as alterações devem acontecer fora do ./tsp-output/server/src/generated diretório.

  1. No diretório, adicione o ./tsp-output/serverAzure Cosmos DB ao projeto:

    npm install @azure/cosmos
    
  2. Adicione a biblioteca de Identidade do Azure para autenticar no Azure:

    npm install @azure/identity
    
  3. Crie um ./tsp-output/server/src/azure diretório para armazenar o código-fonte específico do Azure.

  4. Crie o cosmosClient.ts arquivo nesse diretório para criar um objeto de cliente do Cosmos DB e cole o seguinte código:

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

    Note que o ficheiro utiliza o ponto de extremidade, a base de dados e o contêiner. Ele não precisa de uma cadeia de conexão ou chave porque está usando a credencial DefaultAzureCredentialdo Azure Identity . Saiba mais sobre esse método de autenticação segura para ambientes locais e de produção .

  5. Crie um novo controlador ./tsp-output/server/src/controllers/WidgetsCosmos.tsde widget e cole o seguinte código de integração para o 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. Atualize o ./tsp-output/server/src/index.ts para importar o novo controlador, obtenha as configurações do ambiente do Azure Cosmos DB e, em seguida, crie o WidgetsCosmosController e passe para o roteador.

    // 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. Em um terminal no ./tsp-output/server, compile o TypeScript em JavaScript.

    tsc
    

    O projeto agora é construído com integração com o Cosmos DB. Vamos criar os scripts de implantação para criar os recursos do Azure e implantar o projeto.

Criar infraestrutura de implantação

Crie os ficheiros necessários para ter uma implantação repetível com o CLI do Azure Developer e os modelos Bicep.

  1. Na raiz do projeto TypeSpec, crie um azure.yaml ficheiro de definição de implantação e cole o seguinte 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 essa configuração faz referência a todo o projeto TypeSpec.

  2. Na raiz do projeto TypeSpec, crie o ./Dockerfile que é usado para criar o contêiner para Aplicativos de Contêiner do Azure.

    # 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. Na raiz do projeto TypeSpec, crie um ./infra diretório.

  4. Crie um ./infra/main.bicepparam arquivo e copie o seguinte para definir os parâmetros necessários para a implantação:

    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 fornece os parâmetros mínimos necessários para esta implantação.

  5. Crie um ./infra/main.bicep arquivo e copie o seguinte para definir os recursos do Azure para provisionamento e implantação:

    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
    

    As variáveis OUTPUT permitem que você use os recursos de nuvem provisionados com seu desenvolvimento local.

Implantar aplicativo no Azure

Você pode implantar este aplicativo no Azure usando os Aplicativos de Contêiner do Azure:

  1. Em um terminal na raiz do projeto, autentique-se na CLI do Desenvolvedor do Azure:

    azd auth login
    
  2. Implante em Aplicações de Contentores do Azure usando a CLI do Azure Developer:

    azd up
    
  3. Responda às seguintes perguntas usando as respostas fornecidas.

    • Insira um nome de ambiente exclusivo: tsp-server-js
    • Selecione uma Assinatura do Azure para usar: selecione sua assinatura
    • Selecione um local do Azure para usar: selecione um local perto de você
    • Escolha um grupo de recursos para usar: Selecione Criar um novo grupo de recursos
    • Insira um nome para o novo grupo de recursos: aceite o padrão fornecido
  4. Aguarde até que a implantação seja concluída. A resposta inclui informações semelhantes às seguintes:

    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.
    

Use o aplicativo no navegador

Uma vez implantado, você pode:

  1. No console, selecione o Endpoint url para abri-lo em um navegador.
  2. Adicione a rota, /.api-docs, ao ponto de extremidade para usar o Swagger UI.
  3. Use o recurso Experimente agora em cada método para criar, ler, atualizar e excluir widgets por meio da API.

Aumente a sua aplicação

Agora que você tem todo o processo de ponta a ponta funcionando, continue a criar sua API:

  • Saiba mais sobre a linguagem TypeSpec para adicionar mais APIs e funcionalidades à camada de API no ./main.tsp.
  • Adicione mais emissores e configure seus parâmetros no ./tspconfig.yaml.
  • À medida que você adiciona mais recursos em seus arquivos TypeSpec, ofereça suporte a essas alterações com código-fonte no projeto de servidor.
  • Continue a usar a autenticação sem senha com a Identidade do Azure.

Limpeza de recursos

Quando terminar este início rápido, você poderá remover os recursos do Azure:

azd down

Ou exclua o grupo de recursos diretamente do portal do Azure.

Próximos passos