Dela via


Snabbstart: Skapa ett nytt API-projekt med TypeSpec och TypeScript

I den här snabbstarten: Lär dig hur du använder TypeSpec för att utforma, generera och implementera ett RESTful TypeScript API-program. TypeSpec är ett språk med öppen källkod för att beskriva API:er för molntjänster och genererar klient- och serverkod för flera plattformar. Genom att följa den här snabbstarten får du lära dig hur du definierar ditt API-kontrakt en gång och genererar konsekventa implementeringar, vilket hjälper dig att skapa mer underhållsbara och väldokumenterade API-tjänster.

I den här snabbstarten kommer du att göra följande:

  • Definiera ditt API med TypeSpec
  • Skapa ett API-serverprogram
  • Integrera Azure Cosmos DB för beständig lagring
  • Distribuera till Azure
  • Köra och testa ditt API

Important

@typespec/http-server-js emitter är för närvarande i FÖRHANDSVERSION. Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps. Microsoft lämnar inga garantier, uttryckta eller underförstådda, med avseende på den information som tillhandahålls här.

Prerequisites

Utveckla med TypeSpec

TypeSpec definierar ditt API på ett språkagnostiskt sätt och genererar API-servern och klientbiblioteket för flera plattformar. Med den här funktionen kan du:

  • Definiera ditt API-kontrakt en gång
  • Generera konsekvent server- och klientkod
  • Fokusera på att implementera affärslogik i stället för API-infrastruktur

TypeSpec tillhandahåller API-tjänsthantering:

  • API-definitionsspråk
  • Mellanprogram för routning på serversidan för API
  • Klientbibliotek för användning av API

Du tillhandahåller klientbegäranden och serverintegreringar:

  • Implementera affärslogik i mellanprogram som Azure-tjänster för databaser, lagring och meddelanden
  • Värdserver för ditt API (lokalt eller i Azure)
  • Distributionsskript för upprepningsbar etablering och distribution

Skapa ett nytt TypeSpec-program

  1. Skapa en ny mapp för att lagra API-servern och TypeSpec-filerna.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Installera TypeSpec-kompilatorn globalt:

    npm install -g @typespec/compiler
    
  3. Kontrollera att TypeSpec är korrekt installerat:

    tsp --version
    
  4. Initiera TypeSpec-projektet:

    tsp init
    
  5. Besvara följande frågor med de svar som tillhandahålls:

    • Initiera ett nytt projekt här? Y
    • Välj en projektmall Allmänt REST-API
    • Ange ett projektnamn: Widgetar
    • Vilka sändare vill du använda?
      • OpenAPI 3.1-dokument
      • JavaScript-serverstubbar

    TypeSpec-utsändare är bibliotek som använder olika TypeSpec-kompilator-API:er för att reflektera över TypeSpec-kompileringsprocessen och generera artefakter.

  6. Vänta tills initieringen har slutförts innan du fortsätter.

  7. Kompilera projektet:

    tsp compile .
    
  8. TypeSpec genererar standardprojektet i ./tsp-outputoch skapar två separata mappar:

    • schema är OpenApi 3-specifikationen. Observera att de få raderna i ./main.tsp genererade över 200 rader med OpenApi-specifikation åt dig.
    • server är det genererade mellanprogrammet. Det här mellanprogrammet kan införlivas i ett Node.js serverprojekt.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts definierar gränssnitten för Widgets-API:et.
      • ./tsp-output/js/src/generated/http/openapi3.ts definierar Open API-specifikationen som en TypeScript-fil och återskapas varje gång du kompilerar TypeSpec-projektet.

Konfigurera TypeSpec-utsändare

Använd TypeSpec-filerna för att konfigurera API-servergenereringen för att skapa hela Express.js servern.

  1. Öppna ./tsconfig.yaml och ersätt den befintliga konfigurationen med följande 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
    

    Den här konfigurationen skapar en fullständig Express.js API-server:

    • express: Generera Express.js API-servern, inklusive Swagger-användargränssnittet.
    • emitter-output-dir: Generera allt till ./server katalogen.
  2. Ta bort den befintliga ./tsp-output. Oroa dig inte, du genererar servern i nästa steg.

  3. Använd TypeSpec JavaScript-avsändare för att skapa Express.js-servern:

    npx hsjs-scaffold
    
  4. Ändra till den nya ./tsp-output/server katalogen:

    cd ./tsp-output/server
    
  5. Kompilera TypeScript till JavaScript.

    tsc
    
  6. Kör projektet:

    npm start
    

    Vänta tills meddelandet har öppnats i webbläsaren.

  7. Öppna webbläsaren och gå till http://localhost:3000/.api-docs.

    Skärmbild av webbläsaren som visar Swagger UI för Widgets API.

  8. Standard-TypeSpec-API:et och servern fungerar båda. Om du vill slutföra den här API-servern lägger du till din affärslogik för att stödja Widgets-API:erna i ./tsp-output/server/src/controllers/widgets.ts. Användargränssnittet är anslutet till API:et som returnerar hårdkodade falska data.

Förstå programfilstrukturen

Den Express.js projektstrukturen som finns på tsp-output/server/ inkluderar den genererade servern, package.jsonoch mellanprogrammet för din Azure-integrering.

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

Filstrukturen för det överordnade TypeSpec-projektet innehåller det här Express.js projektet i tsp-output:

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

Ändra beständighet till Azure Cosmos DB no-sql

Nu när den grundläggande Express.js API-servern fungerar uppdaterar du Express.js-servern så att den fungerar med Azure Cosmos DB för ett beständigt datalager. Detta inkluderar ändringar i index.ts för att använda Cosmos DB-integration i mellanprogramvara. Alla ändringar bör ske utanför ./tsp-output/server/src/generated katalogen.

  1. ./tsp-output/server Lägg till Azure Cosmos DB i projektet i katalogen:

    npm install @azure/cosmos
    
  2. Lägg till Azure Identity-biblioteket för att autentisera till Azure:

    npm install @azure/identity
    
  3. Skapa en ./tsp-output/server/src/azure katalog som innehåller källkod som är specifik för Azure.

  4. Skapa filen i katalogen cosmosClient.ts för att skapa ett Cosmos DB-klientobjekt och klistra in följande kod:

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

    Observera att filen använder slutpunkten, databasen och containern. Den behöver ingen anslutningssträng eller nyckel eftersom den använder Azure Identity-autentiseringsuppgiften DefaultAzureCredential. Läs mer om den här metoden för säker autentisering för både lokala miljöer och produktionsmiljöer .

  5. Skapa en ny Widget-styrenhet ./tsp-output/server/src/controllers/WidgetsCosmos.tsoch klistra in följande integreringskod för 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. ./tsp-output/server/src/index.ts Uppdatera för att importera den nya kontrollanten, hämta miljöinställningarna för Azure Cosmos DB och skapa sedan WidgetsCosmosController och skicka till routern.

    // 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. Kompilera TypeScript till JavaScript i en terminal på ./tsp-output/server.

    tsc
    

    Projektet byggs nu med Cosmos DB-integrering. Nu ska vi skapa distributionsskripten för att skapa Azure-resurserna och distribuera projektet.

Skapa distributionsinfrastruktur

Skapa de filer som behövs för att ha en upprepningsbar distribution med Azure Developer CLI - och Bicep-mallar.

  1. I roten för TypeSpec-projektet skapar du en azure.yaml distributionsdefinitionsfil och klistrar in följande källa:

    # 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
    

    Observera att den här konfigurationen refererar till hela TypeSpec-projektet.

  2. I roten för TypeSpec-projektet skapar du den ./Dockerfile som används för att skapa containern för 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. Skapa en ./infra katalog i roten i TypeSpec-projektet.

  4. Skapa en ./infra/main.bicepparam fil och kopiera i följande för att definiera de parametrar vi behöver för distributionen:

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

    Den här paramlistan innehåller de minimiparametrar som behövs för den här distributionen.

  5. Skapa en ./infra/main.bicep fil och kopiera i följande för att definiera Azure-resurser för etablering och distribution:

    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
    

    Med utdatavariablerna kan du använda de etablerade molnresurserna med din lokala utveckling.

Distribuera programmet till Azure

Du kan distribuera det här programmet till Azure med Hjälp av Azure Container Apps:

  1. Autentisera till Azure Developer CLI i en terminal i projektets rot:

    azd auth login
    
  2. Distribuera till Azure Container Apps med hjälp av Azure Developer CLI:

    azd up
    
  3. Besvara följande frågor med de svar som tillhandahålls:

    • Ange ett unikt miljönamn: tsp-server-js
    • Välj en Azure-prenumeration som ska användas: välj din prenumeration
    • Välj en Azure-plats att använda: välj en plats nära dig
    • Välj en resursgrupp som ska användas: Välj Skapa en ny resursgrupp
    • Ange ett namn för den nya resursgruppen: acceptera standardinställningen
  4. Vänta tills distributionen är klar. Svaret innehåller information som liknar följande:

    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.
    

Använda programmet i webbläsaren

När du har distribuerat kan du:

  1. I konsolen väljer du Endpoint-url:en för att öppna den i en webbläsare.
  2. Lägg till vägen, /.api-docs, till slutpunkten för att använda Swagger-användargränssnittet.
  3. Använd funktionen Prova nu på varje metod för att skapa, läsa, uppdatera och ta bort widgetar via API:et.

Utveckla din applikation

Nu när hela processen från slutpunkt till slutpunkt fungerar fortsätter du att skapa ditt API:

  • Läs mer om TypeSpec-språket för att lägga till fler API:er och API-lagerfunktioner i ./main.tsp.
  • Lägg till fler emittenter och konfigurera deras parametrar i ./tspconfig.yaml.
  • När du lägger till fler funktioner i TypeSpec-filerna stöder du dessa ändringar med källkod i serverprojektet.
  • Fortsätt att använda lösenordslös autentisering med Azure Identity.

Rensa resurser

När du är klar med den här snabbstarten kan du ta bort Azure-resurserna:

azd down

Eller ta bort resursgruppen direkt från Azure-portalen.

Next steps