Bagikan melalui


Mulai Cepat: Membuat proyek API baru dengan TypeSpec dan TypeScript

Dalam panduan cepat ini: pelajari cara menggunakan TypeSpec untuk merancang, menghasilkan, dan menerapkan aplikasi API TypeScript RESTful. TypeSpec adalah bahasa sumber terbuka untuk menjelaskan API layanan cloud dan menghasilkan kode klien dan server untuk beberapa platform. Dengan mengikuti mulai cepat ini, Anda mempelajari cara menentukan kontrak API sekali dan menghasilkan implementasi yang konsisten, membantu Anda membangun layanan API yang lebih terawat dan terdokumen dengan baik.

Dalam panduan singkat ini, Anda akan:

  • Tentukan API Anda menggunakan TypeSpec
  • Membuat aplikasi server API
  • Mengintegrasikan Azure Cosmos DB untuk penyimpanan persisten
  • Sebarkan ke Azure
  • Menjalankan dan menguji API Anda

Important

@typespec/http-server-js pemancar saat ini dalam PRATINJAU. Informasi ini berkaitan dengan produk prarilis yang mungkin dimodifikasi secara substansial sebelum dirilis. Microsoft tidak memberikan jaminan, tersurat maupun tersirat, sehubungan dengan informasi yang diberikan di sini.

Prerequisites

Mengembangkan dengan TypeSpec

TypeSpec mendefinisikan API Anda dengan cara bahasa agnostik dan menghasilkan server API dan pustaka klien untuk beberapa platform. Fungsionalitas ini memungkinkan Anda untuk:

  • Tentukan kontrak API Anda sekali
  • Hasilkan server dan kode klien yang konsisten
  • Fokus pada penerapan logika bisnis daripada infrastruktur API

TypeSpec menyediakan manajemen layanan API:

  • Bahasa definisi API
  • Middleware untuk perutean sisi server bagi API
  • Pustaka bagi klien untuk memanfaatkan API

Anda menyediakan permintaan klien dan integrasi server:

  • Menerapkan logika bisnis di middleware seperti layanan Azure untuk database, penyimpanan, dan olahpesan
  • Hosting server untuk API Anda (secara lokal atau di Azure)
  • Skrip penyebaran untuk penyediaan dan penyebaran berulang

Membuat aplikasi TypeSpec baru

  1. Buat folder baru untuk menyimpan server API dan file TypeSpec.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Instal pengkompilasi TypeSpec secara global:

    npm install -g @typespec/compiler
    
  3. Periksa TypeSpec yang terinstal dengan benar:

    tsp --version
    
  4. Inisialisasi proyek TypeSpec:

    tsp init
    
  5. Jawab perintah berikut dengan jawaban yang disediakan:

    • Menginisialisasi proyek baru di sini? Y
    • Pilih templat proyek? API REST umum
    • Masukkan nama proyek: Widget
    • Emiter apa yang ingin Anda gunakan?
      • Dokumen OpenAPI 3.1
      • Stub JavaScript server

    TypeSpec emitters adalah pustaka yang menggunakan berbagai API kompilator TypeSpec untuk merefleksikan proses kompilasi TypeSpec dan menghasilkan artefak.

  6. Tunggu hingga inisialisasi selesai sebelum melanjutkan.

  7. Kompilasi proyek:

    tsp compile .
    
  8. TypeSpec menghasilkan proyek default di ./tsp-output, membuat dua folder terpisah:

    • skema adalah spesifikasi OpenApi 3. Perhatikan bahwa beberapa baris dalam ./main.tsp dihasilkan lebih dari 200 baris spesifikasi OpenApi untuk Anda.
    • server adalah middleware yang dihasilkan. Middleware ini dapat dimasukkan ke dalam proyek server Node.js.
      • ./tsp-output/js/src/generated/models/all/demo-service.ts menentukan antarmuka untuk WIDGET API.
      • ./tsp-output/js/src/generated/http/openapi3.ts menentukan spesifikasi Open API sebagai file TypeScript dan diregenerasi setiap kali Anda mengkompilasi proyek TypeSpec Anda.

Mengonfigurasi pengemisi TypeSpec

Gunakan file TypeSpec untuk mengonfigurasi pembuatan server API untuk membuat perancah seluruh server Express.js.

  1. ./tsconfig.yaml Buka dan ganti konfigurasi yang ada dengan YAML berikut:

    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
    

    Konfigurasi ini membuat server API Express.js lengkap:

    • express: Hasilkan server API Express.js, termasuk antarmuka pengguna Swagger.
    • emitter-output-dir: Hasilkan semuanya ke direktori ./server .
  2. Hapus ./tsp-output yang ada. Jangan khawatir, Anda akan menghasilkan server di langkah berikutnya.

  3. Gunakan pemancar TypeSpec JavaScript untuk membuat server Express.js:

    npx hsjs-scaffold
    
  4. Ubah ke direktori baru ./tsp-output/server :

    cd ./tsp-output/server
    
  5. Kompilasikan TypeScript ke dalam JavaScript.

    tsc
    
  6. Jalankan proyek:

    npm start
    

    Tunggu pemberitahuan untuk Membuka di browser.

  7. Buka browser dan buka http://localhost:3000/.api-docs.

    Cuplikan layar browser yang menampilkan UI Swagger untuk Widget API.

  8. TypeSpec API dan server default berfungsi. Jika Anda ingin menyelesaikan server API ini, tambahkan logika bisnis Anda untuk mendukung API Widget di ./tsp-output/server/src/controllers/widgets.ts. UI terhubung ke API yang mengembalikan data palsu yang dikodekan secara permanen.

Memahami struktur file aplikasi

Struktur proyek Express.js yang ditemukan di tsp-output/server/ mencakup server yang dihasilkan, package.json, dan middleware untuk integrasi Azure Anda.

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

Struktur file untuk proyek TypeSpec induk mencakup proyek Express.js ini di tsp-output:

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

Mengubah persistensi ke Azure Cosmos DB no-sql

Sekarang setelah server API Express.js dasar berfungsi, perbarui server Express.js untuk bekerja dengan Azure Cosmos DB untuk penyimpanan data persisten. Ini termasuk perubahan pada index.ts untuk menggunakan integrasi Cosmos DB di middleware. Semua perubahan harus terjadi di luar direktori ./tsp-output/server/src/generated.

  1. ./tsp-output/server Di direktori, tambahkan Azure Cosmos DB ke proyek:

    npm install @azure/cosmos
    
  2. Tambahkan pustaka Azure Identity untuk mengautentikasi ke Azure:

    npm install @azure/identity
    
  3. Buat ./tsp-output/server/src/azure direktori untuk menyimpan kode sumber khusus untuk Azure.

  4. Buat file di direktori tersebut cosmosClient.ts untuk membuat objek klien Cosmos DB dan tempelkan dalam kode berikut:

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

    Perhatikan bahwa file menggunakan titik akhir, database, dan kontainer. Ini tidak memerlukan string koneksi atau kunci karena menggunakan kredensial DefaultAzureCredentialAzure Identity . Pelajari selengkapnya tentang metode autentikasi aman ini untuk lingkungan lokal dan produksi .

  5. Buat pengontrol Widget baru, ./tsp-output/server/src/controllers/WidgetsCosmos.ts, dan tempelkan kode integrasi berikut untuk 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. Perbarui ./tsp-output/server/src/index.ts untuk mengimpor pengontrol baru, dapatkan pengaturan lingkungan Azure Cosmos DB, lalu buat WidgetsCosmosController dan sampaikan ke router.

    // Generated by Microsoft TypeSpec
    
    import { WidgetsCosmosController } from "./controllers/WidgetsCosmos.js";
    
    import { createDemoServiceRouter } from "./generated/http/router.js";
    
    import express from "express";
    
    import morgan from "morgan";
    
    import { addSwaggerUi } from "./swagger-ui.js";
    
    const azureCosmosEndpoint = process.env.AZURE_COSMOS_ENDPOINT!;
    const azureCosmosDatabase = "WidgetDb";
    const azureCosmosContainer = "Widgets";
    const azureCosmosPartitionKey = "/Id";
    
    const router = createDemoServiceRouter(
      new WidgetsCosmosController(
        azureCosmosEndpoint, 
        azureCosmosDatabase, 
        azureCosmosContainer, 
        azureCosmosPartitionKey)
    );
    const PORT = process.env.PORT || 3000;
    
    const app = express();
    
    app.use(morgan("dev"));
    
    const SWAGGER_UI_PATH = process.env.SWAGGER_UI_PATH || "/.api-docs";
    
    addSwaggerUi(SWAGGER_UI_PATH, app);
    
    app.use(router.expressMiddleware);
    
    app.listen(PORT, () => {
      console.log(`Server is running at http://localhost:${PORT}`);
      console.log(
        `API documentation is available at http://localhost:${PORT}${SWAGGER_UI_PATH}`,
      );
    });
    
  7. Di terminal ./tsp-output/server, kompilasi TypeScript menjadi JavaScript.

    tsc
    

    Proyek ini sekarang dibangun dengan integrasi Cosmos DB. Mari kita buat skrip penyebaran untuk membuat sumber daya Azure dan menyebarkan proyek.

Membuat infrastruktur penyebaran

Buat file yang diperlukan untuk melakukan penyebaran berulang dengan Azure Developer CLI dan templat Bicep.

  1. Di akar proyek TypeSpec, buat azure.yaml file definisi penyebaran dan tempelkan di sumber berikut:

    # 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
    

    Perhatikan bahwa konfigurasi ini mereferensikan seluruh proyek TypeSpec.

  2. Di akar proyek TypeSpec, buat ./Dockerfile yang digunakan untuk membangun kontainer untuk 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. Di akar proyek TypeSpec, buat ./infra direktori.

  4. Buat file ./infra/main.bicepparam dan salin konten berikut untuk menentukan parameter yang kita butuhkan untuk deployment:

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

    Daftar param ini menyediakan parameter minimum yang diperlukan untuk penyebaran ini.

  5. Buat file ./infra/main.bicep dan salin yang berikut ini untuk menentukan sumber daya Azure untuk provisi dan penyebaran.

    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
    

    Variabel OUTPUT memungkinkan Anda menggunakan sumber daya cloud yang disediakan dengan pengembangan lokal Anda.

Menyebarkan aplikasi ke Azure

Anda dapat menyebarkan aplikasi ini ke Azure menggunakan Azure Container Apps:

  1. Di terminal di akar proyek, autentikasi ke Azure Developer CLI:

    azd auth login
    
  2. Sebarkan ke Azure Container Apps menggunakan Azure Developer CLI:

    azd up
    
  3. Jawab perintah berikut dengan jawaban yang disediakan:

    • Masukkan nama lingkungan yang unik: tsp-server-js
    • Pilih Langganan Azure yang akan digunakan: pilih langganan Anda
    • Pilih lokasi Azure yang akan digunakan: pilih lokasi di dekat Anda
    • Pilih grup sumber daya yang akan digunakan: Pilih Buat grup sumber daya baru
    • Masukkan nama untuk grup sumber daya baru: terima default yang disediakan
  4. Tunggu hingga proses penyebaran selesai. Respons mencakup informasi yang mirip dengan yang berikut ini:

    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.
    

Menggunakan aplikasi di browser

Setelah disebarkan, Anda dapat:

  1. Di konsol, pilih Endpoint url untuk membukanya di browser.
  2. Tambahkan rute, /.api-docs, ke titik akhir untuk menggunakan antarmuka pengguna Swagger.
  3. Gunakan fitur Coba sekarang pada setiap metode untuk membuat, membaca, memperbarui, dan menghapus widget melalui API.

Kembangkan aplikasi Anda

Sekarang setelah Anda memiliki seluruh proses end-to-end yang berfungsi, terus bangun API Anda:

  • Pelajari selengkapnya tentang bahasa TypeSpec untuk menambahkan lebih banyak API dan fitur lapisan API di ./main.tsp.
  • Tambahkan lebih banyak emiter dan konfigurasikan parameternya di ./tspconfig.yaml.
  • Saat Anda menambahkan lebih banyak fitur dalam file TypeSpec Anda, dukung perubahan tersebut dengan kode sumber di proyek server.
  • Terus gunakan autentikasi tanpa kata sandi dengan Azure Identity.

Membersihkan sumber daya

Setelah selesai dengan panduan cepat ini, Anda dapat menghapus sumber daya Azure.

azd down

Atau hapus grup sumber daya langsung dari portal Microsoft Azure.

Langkah selanjutnya