umbraco-openapi-client

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Umbraco OpenAPI Client Setup

Umbraco OpenAPI客户端设置

CRITICAL: Why This Matters

重要提示:为何这很关键

NEVER use raw
fetch()
calls for Umbraco backoffice API communication.
Raw fetch calls will result in 401 Unauthorized errors because they don't include the bearer token authentication that Umbraco requires.
ALWAYS use a generated OpenAPI client configured with Umbraco's auth context. This ensures:
  • Proper bearer token authentication
  • Type-safe API calls
  • Automatic token refresh handling
绝对不要在Umbraco后台API通信中使用原生
fetch()
调用。
原生fetch调用会返回401未授权错误,因为它们不包含Umbraco所需的Bearer令牌认证。
务必使用配置了Umbraco认证上下文的生成式OpenAPI客户端,这能确保:
  • 正确的Bearer令牌认证
  • 类型安全的API调用
  • 自动令牌刷新处理

When to Use This

使用场景

Use this pattern whenever you:
  • Create custom C# API controllers with
    [BackOfficeRoute]
  • Need to call your custom APIs from the backoffice frontend
  • Build trees, workspaces, or any UI that loads data from custom endpoints
当你遇到以下情况时,请使用此模式:
  • 创建带有
    [BackOfficeRoute]
    特性的自定义C# API控制器
  • 需要从后台前端调用自定义API
  • 构建树、工作区或任何从自定义端点加载数据的UI

Setup Overview

设置概述

The setup has 4 parts:
  1. C# Backend: Controller with Swagger/OpenAPI documentation
  2. Client Dependencies:
    @hey-api/openapi-ts
    and
    @hey-api/client-fetch
  3. Generation Script: Fetches swagger.json and generates TypeScript client
  4. Entry Point Configuration: Configures client with Umbraco auth
设置分为4个部分:
  1. C#后端:带有Swagger/OpenAPI文档的控制器
  2. 客户端依赖
    @hey-api/openapi-ts
    @hey-api/client-fetch
  3. 生成脚本:获取swagger.json并生成TypeScript客户端
  4. 入口点配置:使用Umbraco认证信息配置客户端

Step-by-Step Implementation

分步实现

1. C# Backend Setup (Swagger/OpenAPI)

1. C#后端设置(Swagger/OpenAPI)

Your API must be exposed via Swagger. Create a composer:
csharp
// Composers/MyApiComposer.cs
using Asp.Versioning;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Web.Common.ApplicationBuilder;

namespace MyExtension.Composers;

public class MyApiComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        // Register the API and Swagger
        builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
        builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, MySwaggerGenOptions>();
        builder.Services.Configure<UmbracoPipelineOptions>(options =>
        {
            options.AddFilter(new UmbracoPipelineFilter(Constants.ApiName)
            {
                SwaggerPath = $"/umbraco/swagger/{Constants.ApiName.ToLower()}/swagger.json",
                SwaggerRoutePrefix = $"{Constants.ApiName.ToLower()}",
            });
        });
    }
}

// Swagger schema ID handler
public class MySchemaIdHandler : SchemaIdHandler
{
    public override bool CanHandle(Type type)
        => type.Namespace?.StartsWith("MyExtension") ?? false;
}

// Swagger generation options
public class MySwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
    public void Configure(SwaggerGenOptions options)
    {
        options.SwaggerDoc(
            Constants.ApiName,
            new OpenApiInfo
            {
                Title = "My Extension API",
                Version = "1.0",
            });
    }
}

// Constants
public static class Constants
{
    public const string ApiName = "myextension";
}
你的API必须通过Swagger暴露。创建一个Composer:
csharp
// Composers/MyApiComposer.cs
using Asp.Versioning;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Web.Common.ApplicationBuilder;

namespace MyExtension.Composers;

public class MyApiComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        // Register the API and Swagger
        builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
        builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, MySwaggerGenOptions>();
        builder.Services.Configure<UmbracoPipelineOptions>(options =>
        {
            options.AddFilter(new UmbracoPipelineFilter(Constants.ApiName)
            {
                SwaggerPath = $"/umbraco/swagger/{Constants.ApiName.ToLower()}/swagger.json",
                SwaggerRoutePrefix = $"{Constants.ApiName.ToLower()}",
            });
        });
    }
}

// Swagger schema ID handler
public class MySchemaIdHandler : SchemaIdHandler
{
    public override bool CanHandle(Type type)
        => type.Namespace?.StartsWith("MyExtension") ?? false;
}

// Swagger generation options
public class MySwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
    public void Configure(SwaggerGenOptions options)
    {
        options.SwaggerDoc(
            Constants.ApiName,
            new OpenApiInfo
            {
                Title = "My Extension API",
                Version = "1.0",
            });
    }
}

// Constants
public static class Constants
{
    public const string ApiName = "myextension";
}

2. Client package.json Dependencies

2. Client package.json依赖

Add to your
Client/package.json
:
json
{
  "scripts": {
    "generate-client": "node scripts/generate-openapi.js https://localhost:44325/umbraco/swagger/myextension/swagger.json"
  },
  "devDependencies": {
    "@hey-api/client-fetch": "^0.10.0",
    "@hey-api/openapi-ts": "^0.66.7",
    "chalk": "^5.4.1",
    "node-fetch": "^3.3.2"
  }
}
添加到你的
Client/package.json
json
{
  "scripts": {
    "generate-client": "node scripts/generate-openapi.js https://localhost:44325/umbraco/swagger/myextension/swagger.json"
  },
  "devDependencies": {
    "@hey-api/client-fetch": "^0.10.0",
    "@hey-api/openapi-ts": "^0.66.7",
    "chalk": "^5.4.1",
    "node-fetch": "^3.3.2"
  }
}

3. Generation Script

3. 生成脚本

Create
Client/scripts/generate-openapi.js
:
javascript
import fetch from "node-fetch";
import chalk from "chalk";
import { createClient, defaultPlugins } from "@hey-api/openapi-ts";

console.log(chalk.green("Generating OpenAPI client..."));

const swaggerUrl = process.argv[2];
if (swaggerUrl === undefined) {
  console.error(chalk.red(`ERROR: Missing URL to OpenAPI spec`));
  process.exit(1);
}

// Ignore self-signed certificates on localhost
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

console.log(`Fetching OpenAPI definition from ${chalk.yellow(swaggerUrl)}`);

fetch(swaggerUrl)
  .then(async (response) => {
    if (!response.ok) {
      console.error(chalk.red(`ERROR: ${response.status} ${response.statusText}`));
      return;
    }

    await createClient({
      input: swaggerUrl,
      output: "src/api",
      plugins: [
        ...defaultPlugins,
        {
          name: "@hey-api/client-fetch",
          bundle: true,
          exportFromIndex: true,
          throwOnError: true,
        },
        {
          name: "@hey-api/typescript",
          enums: "typescript",
        },
        {
          name: "@hey-api/sdk",
          asClass: true,
        },
      ],
    });

    console.log(chalk.green("Client generated successfully!"));
  })
  .catch((error) => {
    console.error(`ERROR: ${chalk.red(error.message)}`);
  });
创建
Client/scripts/generate-openapi.js
javascript
import fetch from "node-fetch";
import chalk from "chalk";
import { createClient, defaultPlugins } from "@hey-api/openapi-ts";

console.log(chalk.green("Generating OpenAPI client..."));

const swaggerUrl = process.argv[2];
if (swaggerUrl === undefined) {
  console.error(chalk.red(`ERROR: Missing URL to OpenAPI spec`));
  process.exit(1);
}

// Ignore self-signed certificates on localhost
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

console.log(`Fetching OpenAPI definition from ${chalk.yellow(swaggerUrl)}`);

fetch(swaggerUrl)
  .then(async (response) => {
    if (!response.ok) {
      console.error(chalk.red(`ERROR: ${response.status} ${response.statusText}`));
      return;
    }

    await createClient({
      input: swaggerUrl,
      output: "src/api",
      plugins: [
        ...defaultPlugins,
        {
          name: "@hey-api/client-fetch",
          bundle: true,
          exportFromIndex: true,
          throwOnError: true,
        },
        {
          name: "@hey-api/typescript",
          enums: "typescript",
        },
        {
          name: "@hey-api/sdk",
          asClass: true,
        },
      ],
    });

    console.log(chalk.green("Client generated successfully!"));
  })
  .catch((error) => {
    console.error(`ERROR: ${chalk.red(error.message)}`);
  });

4. Entry Point Configuration (CRITICAL)

4. 入口点配置(关键)

Configure the generated client with Umbraco's auth context in your entry point:
typescript
// src/entrypoints/entrypoint.ts
import type { UmbEntryPointOnInit, UmbEntryPointOnUnload } from "@umbraco-cms/backoffice/extension-api";
import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth";
import { client } from "../api/client.gen.js";

export const onInit: UmbEntryPointOnInit = (host, _extensionRegistry) => {
  // CRITICAL: Configure the OpenAPI client with authentication
  host.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
    if (!authContext) return;

    const config = authContext.getOpenApiConfiguration();

    client.setConfig({
      baseUrl: config.base,
      credentials: config.credentials,
      auth: config.token,  // This provides the bearer token!
    });

    console.log("API client configured with auth");
  });
};

export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
  // Cleanup if needed
};
在你的入口点中使用Umbraco的认证上下文配置生成的客户端:
typescript
// src/entrypoints/entrypoint.ts
import type { UmbEntryPointOnInit, UmbEntryPointOnUnload } from "@umbraco-cms/backoffice/extension-api";
import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth";
import { client } from "../api/client.gen.js";

export const onInit: UmbEntryPointOnInit = (host, _extensionRegistry) => {
  // 关键:使用认证信息配置OpenAPI客户端
  host.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
    if (!authContext) return;

    const config = authContext.getOpenApiConfiguration();

    client.setConfig({
      baseUrl: config.base,
      credentials: config.credentials,
      auth: config.token,  // 这会提供Bearer令牌!
    });

    console.log("API client configured with auth");
  });
};

export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
  // 如有需要可进行清理
};

5. Using the Generated Client

5. 使用生成的客户端

After running
npm run generate-client
, use the generated service:
typescript
// In your workspace context, repository, or data source
import { MyExtensionService } from "../api/index.js";

// The client handles auth automatically!
const response = await MyExtensionService.getItems({
  query: { skip: 0, take: 50 },
});

const item = await MyExtensionService.getItem({
  path: { id: "some-guid" },
});

await MyExtensionService.createItem({
  body: { name: "New Item", value: 123 },
});
运行
npm run generate-client
后,使用生成的服务:
typescript
// 在你的工作区上下文、仓库或数据源中
import { MyExtensionService } from "../api/index.js";

// 客户端会自动处理认证!
const response = await MyExtensionService.getItems({
  query: { skip: 0, take: 50 },
});

const item = await MyExtensionService.getItem({
  path: { id: "some-guid" },
});

await MyExtensionService.createItem({
  body: { name: "New Item", value: 123 },
});

Generation Workflow

生成流程

  1. Start Umbraco - The swagger.json endpoint must be accessible
  2. Run generation:
    npm run generate-client
  3. Generated files appear in
    src/api/
    :
    • types.gen.ts
      - TypeScript types from your C# models
    • sdk.gen.ts
      - Service class with typed methods
    • client.gen.ts
      - HTTP client configuration
    • index.ts
      - Re-exports everything
  1. 启动Umbraco - swagger.json端点必须可访问
  2. 运行生成命令
    npm run generate-client
  3. 生成的文件会出现在
    src/api/
    目录中:
    • types.gen.ts
      - 来自C#模型的TypeScript类型
    • sdk.gen.ts
      - 带有类型化方法的服务类
    • client.gen.ts
      - HTTP客户端配置
    • index.ts
      - 重新导出所有内容

Common Mistakes

常见错误

❌ WRONG: Raw fetch

❌ 错误用法:原生fetch

typescript
// This will get 401 Unauthorized!
const response = await fetch('/umbraco/myextension/api/v1/items');
typescript
// 这会返回401未授权错误!
const response = await fetch('/umbraco/myextension/api/v1/items');

❌ WRONG: fetch with credentials only

❌ 错误用法:仅带凭证的fetch

typescript
// Still fails - cookies don't work for Management API
const response = await fetch('/umbraco/myextension/api/v1/items', {
  credentials: 'include'
});
typescript
// 仍然失败 - Cookie不适用于管理API
const response = await fetch('/umbraco/myextension/api/v1/items', {
  credentials: 'include'
});

✅ CORRECT: Generated OpenAPI client

✅ 正确用法:生成的OpenAPI客户端

typescript
// Client is configured with bearer token in entry point
const response = await MyExtensionService.getItems();
typescript
// 客户端已在入口点配置了Bearer令牌
const response = await MyExtensionService.getItems();

Reference Example

参考示例

See the complete working implementation in:
  • examples/notes-wiki/Client/
    - Full OpenAPI client setup
  • examples/tree-example/Client/
    - Tree with OpenAPI integration
可查看完整的可运行实现:
  • examples/notes-wiki/Client/
    - 完整的OpenAPI客户端设置
  • examples/tree-example/Client/
    - 集成OpenAPI的树组件

Key Files to Create

需要创建的关键文件

  1. Composers/MyApiComposer.cs
    - Swagger registration
  2. Client/scripts/generate-openapi.js
    - Generation script
  3. Client/src/entrypoints/entrypoint.ts
    - Auth configuration
  4. Client/src/api/
    - Generated (don't edit manually)
That's it! Always generate your API client and configure it with auth. Never use raw fetch for authenticated endpoints.
  1. Composers/MyApiComposer.cs
    - Swagger注册
  2. Client/scripts/generate-openapi.js
    - 生成脚本
  3. Client/src/entrypoints/entrypoint.ts
    - 认证配置
  4. Client/src/api/
    - 生成的文件(请勿手动编辑)
完成!请始终生成API客户端并配置认证信息。绝对不要在认证端点中使用原生fetch。