Partager via


Démarrage rapide : Créer un projet d’API avec TypeSpec et TypeScript

Dans ce guide de démarrage rapide : découvrez comment utiliser TypeSpec pour concevoir, générer et implémenter une application API TYPEScript RESTful. TypeSpec est un langage open source pour décrire les API de service cloud et génère du code client et serveur pour plusieurs plateformes. En suivant ce guide de démarrage rapide, vous allez apprendre à définir votre contrat d’API une fois et à générer des implémentations cohérentes, ce qui vous aide à créer des services d’API plus faciles à gérer et bien documentés.

Dans ce guide de démarrage rapide, vous :

  • Définir votre API à l’aide de TypeSpec
  • Créer une application de serveur d’API
  • Intégrer Azure Cosmos DB pour le stockage persistant
  • Déployer sur Azure
  • Exécuter et tester votre API

Important

@typespec/http-server-js l’émetteur est actuellement en aperçu. Ces informations concernent un produit en version préliminaire qui peut être sensiblement modifié avant sa commercialisation. Microsoft n’offre aucune garantie, exprimée ou implicite, en ce qui concerne les informations fournies ici.

Prerequisites

Développement avec TypeSpec

TypeSpec définit votre API de manière indépendante du langage et génère le serveur d’API et la bibliothèque cliente pour plusieurs plateformes. Cette fonctionnalité vous permet de :

  • Définir votre contrat d’API une seule fois
  • Générer du code client et serveur cohérent
  • Concentrez-vous sur l’implémentation de la logique métier plutôt que sur l’infrastructure d’API

TypeSpec fournit la gestion des services d’API :

  • Langage de définition d’API
  • Middleware de routage côté serveur pour l’API
  • Bibliothèques clientes pour l’utilisation de l’API

Vous fournissez des demandes clientes et des intégrations de serveur :

  • Implémenter une logique métier dans un intergiciel ( middleware) tel que les services Azure pour les bases de données, le stockage et la messagerie
  • Serveur d’hébergement pour votre API (localement ou dans Azure)
  • Scripts de déploiement pour l’approvisionnement et le déploiement reproductibles

Créer une application TypeSpec

  1. Créez un dossier pour contenir le serveur d’API et les fichiers TypeSpec.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Installez globalement le compilateur TypeSpec :

    npm install -g @typespec/compiler
    
  3. Vérifiez que TypeSpec est installé correctement :

    tsp --version
    
  4. Initialisez le projet TypeSpec :

    tsp init
    
  5. Répondez aux questions ci-dessous avec les réponses fournies.

    • Initialiser un nouveau projet ici ? Y
    • Sélectionnez un modèle de projet ? API REST générique
    • Entrez un nom de projet : Widgets
    • Quels émetteurs voulez-vous utiliser ?
      • Document OpenAPI 3.1
      • Stubs de serveur JavaScript

    Les émetteurs TypeSpec sont des bibliothèques qui utilisent différentes API du compilateur TypeSpec pour réfléchir au processus de compilation TypeSpec et générer des artefacts.

  6. Attendez que l’initialisation se termine avant de continuer.

  7. Compilez le projet :

    tsp compile .
    
  8. TypeSpec génère le projet par défaut dans ./tsp-output, créant deux dossiers distincts :

    • schéma est la spécification OpenApi 3. Notez que les quelques lignes dans ./main.tsp ont généré plus de 200 lignes de spécification OpenApi pour vous.
    • le serveur est l’intergiciel généré. Ce middleware peut être incorporé dans un projet de serveur Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts définit les interfaces de l’API Widgets.
      • ./tsp-output/js/src/generated/http/openapi3.ts définit la spécification open API en tant que fichier TypeScript et est régénérée chaque fois que vous compilez votre projet TypeSpec.

Configurer des émetteurs TypeSpec

Utilisez les fichiers TypeSpec pour configurer la génération du serveur d’API pour générer la structure de l’ensemble du serveur Express.js.

  1. Ouvrez le ./tsconfig.yaml et remplacez la configuration existante par le YAML suivant :

    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
    

    Cette configuration crée un serveur d’API Express.js complet :

    • express: générez le serveur d’API Express.js, y compris l’interface utilisateur Swagger.
    • emitter-output-dir : générer tout dans le répertoire ./server.
  2. Supprimez l’existant ./tsp-output. Ne vous inquiétez pas, vous allez générer le serveur à l’étape suivante.

  3. Utilisez l’émetteur JavaScript TypeSpec pour créer le serveur Express.js :

    npx hsjs-scaffold
    
  4. Passez au nouveau ./tsp-output/server répertoire :

    cd ./tsp-output/server
    
  5. Compilez le TypeScript en JavaScript.

    tsc
    
  6. Exécutez le projet :

    npm start
    

    Attendez que la notification s’ouvre dans le navigateur.

  7. Ouvrez le navigateur et accédez à http://localhost:3000/.api-docs.

    Capture d’écran du navigateur affichant l’interface utilisateur Swagger pour l’API Widgets.

  8. L’API TypeSpec et le serveur par défaut fonctionnent tous les deux. Si vous souhaitez terminer ce serveur d’API, ajoutez votre logique métier pour prendre en charge les API Widgets dans ./tsp-output/server/src/controllers/widgets.ts. L’interface utilisateur est connectée à l’API qui retourne des fausses données codées en dur.

Comprendre la structure des fichiers d’application

La structure de projet Express.js trouvée à tsp-output/server/ inclut le serveur généré, le package.json, et le middleware de l'intégration 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 structure de fichiers du projet TypeSpec parent inclut ce projet Express.js dans tsp-output:

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

Modifier la persistance vers Azure Cosmos DB no-sql

Maintenant que le serveur d’API de base Express.js fonctionne, mettez à jour le serveur Express.js pour qu’il fonctionne avec Azure Cosmos DB pour un magasin de données persistant. Cela inclut les modifications apportées à l’utilisation de l’intégration index.ts de Cosmos DB dans le middleware. Toutes les modifications doivent se produire en dehors du ./tsp-output/server/src/generated répertoire.

  1. Dans le ./tsp-output/server répertoire, ajoutez Azure Cosmos DB au projet :

    npm install @azure/cosmos
    
  2. Ajoutez la bibliothèque d’identités Azure pour vous authentifier auprès d’Azure :

    npm install @azure/identity
    
  3. Créez un ./tsp-output/server/src/azure répertoire pour contenir le code source spécifique à Azure.

  4. Créez le cosmosClient.ts fichier dans ce répertoire pour créer un objet client Cosmos DB et collez le code suivant :

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

    Notez que le fichier utilise le point de terminaison, la base de données et le conteneur. Elle n’a pas besoin d’une chaîne de connexion ou d’une clé, car elle utilise les informations d’identification DefaultAzureCredentiald’identité Azure. En savoir plus sur cette méthode d’authentification sécurisée pour les environnements locaux et de production .

  5. Créez un contrôleur de widget, ./tsp-output/server/src/controllers/WidgetsCosmos.tspuis collez le code d’intégration suivant pour 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. Mettez à jour ./tsp-output/server/src/index.ts pour importer le nouveau contrôleur, obtenez les paramètres d'environnement Azure Cosmos DB, puis créez le WidgetsCosmosController et transmettez-le au routeur.

    // 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. Dans un terminal à l’adresse ./tsp-output/server, compilez le TypeScript en JavaScript.

    tsc
    

    Le projet s’appuie désormais sur l’intégration de Cosmos DB. Nous allons créer les scripts de déploiement pour créer les ressources Azure et déployer le projet.

Créer une infrastructure de déploiement

Créez les fichiers nécessaires pour disposer d’un déploiement reproductible avec azure Developer CLI et des modèles Bicep.

  1. À la racine du projet TypeSpec, créez un fichier de définition de déploiement azure.yaml et collez le texte source suivant :

    # 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
    

    Notez que cette configuration fait référence à l’ensemble du projet TypeSpec.

  2. À la racine du projet TypeSpec, créez le ./Dockerfile, qui est utilisé pour construire le conteneur pour 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. À la racine du projet TypeSpec, créez un ./infra répertoire.

  4. Créez un ./infra/main.bicepparam fichier et copiez-le dans ce qui suit pour définir les paramètres dont nous avons besoin pour le déploiement :

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

    Cette liste de paramètres contient les paramètres minimaux nécessaires pour ce déploiement.

  5. Créez un ./infra/main.bicep fichier et copiez-le dans ce qui suit pour définir les ressources Azure pour l’approvisionnement et le déploiement :

    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
    

    Les variables OUTPUT vous permettent d’utiliser les ressources cloud approvisionnées avec votre développement local.

Déployer une application sur Azure

Vous pouvez déployer cette application sur Azure à l’aide d’Azure Container Apps :

  1. Dans un terminal à la racine du projet, authentifiez-vous auprès d’Azure Developer CLI :

    azd auth login
    
  2. Déployez sur Azure Container Apps à l’aide d’Azure Developer CLI :

    azd up
    
  3. Répondez aux questions ci-dessous avec les réponses fournies.

    • Entrez un nom d’environnement unique : tsp-server-js
    • Sélectionnez un abonnement Azure à utiliser : sélectionnez votre abonnement
    • Sélectionnez un emplacement Azure à utiliser : sélectionnez un emplacement près de vous
    • Choisir un groupe de ressources à utiliser : sélectionner Créer un nouveau groupe de ressources
    • Entrez un nom pour le nouveau groupe de ressources : acceptez la valeur par défaut fournie
  4. Attendez que le déploiement se termine. La réponse inclut des informations similaires à ce qui suit :

    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.
    

Utiliser l’application dans le navigateur

Une fois déployé, vous pouvez :

  1. Dans la console, sélectionnez l’URL Endpoint pour l’ouvrir dans un navigateur.
  2. Ajoutez l’itinéraire, /.api-docsau point de terminaison pour utiliser l’interface utilisateur Swagger.
  3. Utilisez la fonctionnalité Try it now sur chaque méthode pour créer, lire, mettre à jour et supprimer des widgets via l’API.

Développer votre application

Maintenant que vous disposez de l’intégralité du processus de bout en bout, continuez à générer votre API :

  • En savoir plus sur le langage TypeSpec pour ajouter d’autres API et fonctionnalités de couche API dans le ./main.tsp.
  • Ajoutez d’autres émetteurs et configurez leurs paramètres dans le ./tspconfig.yaml.
  • Lorsque vous ajoutez d’autres fonctionnalités dans vos fichiers TypeSpec, prenez en charge ces modifications avec le code source dans le projet de serveur.
  • Continuez à utiliser l’authentification sans mot de passe avec Azure Identity.

Nettoyer les ressources

Lorsque vous avez terminé avec ce guide de démarrage rapide, vous pouvez supprimer les ressources Azure :

azd down

Ou supprimez le groupe de ressources directement à partir du portail Azure.

Étapes suivantes