首页
/ Immich API 体系解析:基于 OpenAPI 的规范自动生成与 TypeScript / Dart 双端 SDK 构建

Immich API 体系解析:基于 OpenAPI 的规范自动生成与 TypeScript / Dart 双端 SDK 构建

2026-09-05 18:18:47作者:贡沫苏Truman

Immich 采用 OpenAPI 标准作为其 API 的单一事实来源:服务器在开发模式下自动导出 OpenAPI 规范文件,再由 mise open-api 任务链一键生成 TypeScript 与 Dart 两套客户端 SDK。本文围绕 Immich 仓库的 API 文档与配套源码,讲清规范文件从 NestJS 控制器装饰器到 immich-openapi-specs.json 的生成机制、mise open-api 的完整执行链路,以及两套 SDK 的产物位置、定制模板与补丁策略,帮助你在新增或修改端点后正确地重建客户端代码。

核心概念:OpenAPI 是文档与 SDK 的共同源头

Immich 的 API 文档与客户端 SDK 均派生自同一份 OpenAPI 规范,而非各自独立维护。这一设计意味着:

  • 文档不手写:公开 API 文档(发布地址为 api.immich.app)由 OpenAPI 规范渲染而来,端点行为变更只需修改服务端控制器,文档与 SDK 随之更新;
  • SDK 不手改:TypeScript 与 Dart 客户端均由 open-api/immich-openapi-specs.json 生成,任何手工修改都会在下次生成时被覆盖。

从仓库中已提交的规范文件 immich-openapi-specs.json 可以看到,它是一份标准 OpenAPI 3.x 文档,顶层包含 openapipathsinfotagsserverscomponents 六个部分,当前共定义了 191 个 API 路径,涵盖 /activities/admin/config/admin/database-backups 等管理端与业务端端点。服务器根前缀为 /apiservers 字段中声明为 /api),因此所有业务端点实际都以 /api/... 暴露。

规范中声明了三种认证机制,这也是调用 Immich API 时最核心的三要素:

认证方式 位置 说明
Bearer Token HTTP Header 用于 OAuth / JWT 类认证
Cookie(Access Token) Cookie Web 端登录态会话
API Key(x-api-key HTTP Header 用于外部集成、CLI 等场景

这三项声明来自服务端 Swagger 文档构建器的配置,下一节将展开。

服务端自动生成规范文件:useSwagger 的实现

规范文件并非静态维护,而是由服务器代码在运行时“反射”自身路由后写出的。核心逻辑位于 useSwagger

export const useSwagger = (app: INestApplication, { write }: { write: boolean }) => {
  const builder = new DocumentBuilder()
    .setTitle('Immich')
    .setDescription('Immich API')
    .setVersion(serverVersion.toString())
    .addBearerAuth({ type: 'http', scheme: 'Bearer', in: 'header' })
    .addCookieAuth(ImmichCookie.AccessToken)
    .addApiKey(
      { type: 'apiKey', in: 'header', name: ImmichHeader.ApiKey },
      MetadataKey.ApiKeySecurity,
    )
    .addServer('/api');

  for (const [tag, description] of Object.entries(endpointTags)) {
    builder.addTag(tag, description);
  }
  const config = builder.build();
  const options: SwaggerDocumentOptions = {
    operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
    extraModels,
    ignoreGlobalPrefix: true,
  };

  const specification = SwaggerModule.createDocument(app, config, options);
  const openApiDoc = cleanupOpenApiDoc(specification);
  // ... SwaggerModule.setup('doc', app, openApiDoc, customOptions);

  if (write) {
    // Generate API Documentation only in development mode
    const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
    writeFileSync(outputPath, JSON.stringify(patchOpenAPI(openApiDoc), null, 2), { encoding: 'utf8' });
  }
};

这里有几个值得注意的实现细节:

  1. 文档内容来自 NestJS 装饰器SwaggerModule.createDocument 扫描所有控制器与 DTO 上的 @nestjs/swagger 装饰器(@ApiTags@ApiProperty@ApiResponse 等)构建规范。因此“修改规范”的正确姿势就是“修改控制器端点及其装饰器”,官方文档(api.md)也明确这一点:immich-openapi-specs.json 由控制器端点使用或引用的 @nestjs/swagger 装饰器决定其内容。
  2. operationId 取方法名operationIdFactory 返回方法键(methodKey),保证生成的 SDK 方法名与控制器方法名一一对应,便于跨端对照源码。
  3. Swagger UI 挂载与导出:文档在 /api/doc 提供交互式界面,同时通过 jsonDocumentUrl: '/api/spec.json'yamlDocumentUrl: '/api/spec.yaml' 暴露 JSON/YAML 原始规范(ignoreGlobalPrefix: true 保证这些路径不受全局前缀影响)。
  4. write 开关:只有 write: true 时才会把(经过 patchOpenAPI 二次修补后的)规范序列化写入 open-api/immich-openapi-specs.json

开发模式下的自动写入

在生产/开发共用的应用启动流程 app.common.ts 中,写入行为被严格门控:

useSwagger(app, { write: configRepository.isDev() && permitSwaggerWrite });

即:只有开发模式(isDev())且允许写入时,服务器运行期间才会在内存/磁盘上导出规范。这解释了官方文档中“规范文件由服务器在开发模式下运行时自动生成”的表述——生产部署默认不重写该文件。

独立的 sync-open-api 脚本

除运行中服务器外,仓库还提供了一个一次性同步脚本 sync-open-api.ts

#!/usr/bin/env node
process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { ApiModule } from 'src/app.module';
import { useSwagger } from 'src/utils/misc';

const sync = async () => {
  const app = await NestFactory.create<NestExpressApplication>(ApiModule, { preview: true });
  useSwagger(app, { write: true });
  await app.close();
};

该脚本用 preview: true(NestJS 预加载模式,不监听端口、不真正启动依赖连接)构建完整 ApiModule,强制 write: true 导出规范后立即关闭应用。顶部的 DB_URL 兜底赋值让脚本无需真实数据库即可构建路由树。编译后的入口为 dist/bin/sync-open-api.js,由 server/mise.toml 中的任务包装:

[tasks."sync-open-api"]
run = "node ./dist/bin/sync-open-api.js"

mise open-api:一键重建双端 SDK 的任务链

官方文档给出的开发流程命令是 mise open-api。它在根目录 mise.toml 中定义为一个任务编排,按依赖顺序串联起整个再生成流水线:

[tasks.open-api]
run = [
  { task = "//:plugins" },
  { task = "//server:install" },
  { task = "//server:build" },
  { task = "//server:sync-open-api" },
  { task = ":open-api-typescript" },
  { task = ":open-api-dart" },
]

执行链路可概括为:构建插件 → 安装并编译服务器 → 用 sync-open-api 刷新 immich-openapi-specs.json → 用 oazapfts 生成 TypeScript 客户端 → 用 OpenAPI Generator 生成 Dart 客户端。前四步保证“规范先行”,后两步保证“双端同步”。

TypeScript 端:oazapfts 生成

[tasks.open-api-typescript]
run = [
  "oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas open-api/immich-openapi-specs.json packages/sdk/src/fetch-client.ts",
  { task = "//:sdk:install" },
  { task = "//:sdk:build" },
]

关键参数含义:

  • --optimistic:对可能缺失的字段采取乐观类型推断,减少运行时的防御性代码;
  • --argumentStyle=object:多参数端点以单个对象字面量传参(client.assets.get({ id }) 风格),提升 SDK 可用性;
  • --useEnumType:OpenAPI 枚举映射为 TS 联合字面量类型而非 enum
  • --allSchemas:导出 components/schemas 中的全部数据模型类型。

生成产物是 packages/sdk/src/fetch-client.ts,文件头明确标注:

/**
 * Immich
 * 3.2.0-rc.0
 * DO NOT MODIFY - This file has been generated using oazapfts.
 */

注意:早期文档表述中 TypeScript SDK 位于 packages/sdk/client,而当前仓库的实际产物路径是 packages/sdk/src/(以本仓库为准)。生成后还会执行 sdk:installpnpm --filter @immich/sdk install --frozen-lockfile)与 sdk:build 完成安装和构建。

Dart 端:OpenAPI Generator + 模板补丁

[tasks.open-api-dart]
dir = "open-api"
sources = [
  "immich-openapi-specs.json",
  "openapitools.json",
  "templates/**/*",
  "patch/*",
  "bin/generate-dart-sdk.sh",
]
outputs = ["../mobile/generated/openapi/"]
run = "bash ./bin/generate-dart-sdk.sh"

Dart 端采用 openapi-generator-cliopenapitools.json 中固定 generator 版本为 7.25.0),输出目录为 mobile/generated/openapi/(与 api.md 描述一致)。为了贴合 Flutter 侧的手写代码,Immich 在 open-api 目录下维护了两类定制资产:

  1. Mustache 模板补丁templates/mobile 下包含 api.mustache.patchserialization/native/native_class.mustache.patch,用于在生成前修改 Dart 代码生成的模板行为;
  2. 产物级 patch 文件open-api/patch 目录存放生成后对产物的统一修补,例如 api_client.dart.patchApiClientbasePathfinal String 改为可变 String,使移动端运行时可以替换 API 地址;asset_edit_action_item_dto.dart.patchapi.dart.patch 等则修正个别 DTO 的类型细节,pubspec_immich_mobile.yaml.patch 调整依赖声明。

相关工具版本同样被 mise 锁定以保证可重复构建(mise.toml):npm:oazapfts = 7.5.0npm:@openapitools/openapi-generator-cli = 2.40.1java = 21.0.2(OpenAPI Generator 为 Java 实现,需要 JDK 环境)。

TypeScript SDK 的手写封装层

生成的 fetch-client.ts 之外,packages/sdk/src/index.ts 是一段手写的薄封装层,负责对外暴露稳定的初始化与鉴权 API:

export const init = ({ baseUrl, apiKey, headers }: InitOptions) => {
  setBaseUrl(baseUrl);
  setApiKey(apiKey);
  if (headers) {
    setHeaders(headers);
  }
};

export const setApiKey = (apiKey: string) => {
  defaults.headers = defaults.headers || {};
  defaults.headers['x-api-key'] = apiKey;
};

要点解析:

  • API Key 走 x-api-keysetApiKey 把密钥写入 x-api-key 请求头,与规范中 addApiKey 声明的服务端校验头完全对应;assertNoApiKey 还禁止通过通用 setHeader 覆盖该头,确保鉴权入口唯一。
  • jsonOnly 响应守卫:默认 fetch 实现被包装为 jsonOnly()(见 fetch-errors.ts),当请求 Accept 含 JSON 而响应 content-type 不是 JSON 时抛出 MalformedResponseError,把“返回了 HTML 错误页”这类故障显性化。
  • 资源路径助手:封装层还导出 getAssetOriginalPathgetAssetThumbnailPathgetAssetPlaybackPath 等路径构造函数,这些“大对象直链”不走 SDK 方法调用,而是由调用方直接拼接 baseUrl + path 访问。

这套分层(生成层 + 手写薄封装)是 Immich 能在每次重新生成后保留定制行为的关键:fetch-client.ts 可随意重建,而 index.ts / fetch-errors.ts 作为手写文件不受生成流程影响。

开发者流程:新增或修改端点后该做什么

综合 api.md 与源码,标准开发流程如下:

  1. server/src/controllers/ 下新增端点或修改现有端点,用 @nestjs/swagger 装饰器完善参数与响应描述(这直接决定规范文件内容);
  2. 确保服务器能以开发模式构建(规范导出依赖编译产物);
  3. 执行 mise open-api,完整跑通“同步规范 → 生成 TS 客户端 → 构建 SDK → 生成 Dart 客户端”的链路;
  4. 检查两份生成产物:packages/sdk/src/fetch-client.tsmobile/generated/openapi/,确认新端点已出现且类型正确;
  5. 若 Dart 生成产物存在类型或行为偏差,优先评估通过 open-api/patch 增加统一补丁,而非手工改动生成文件。

此外,从根 mise.tomlrelease 任务可以看到,版本发布流水线也会强制执行 //:open-api,这保证了每次发版的规范文件与双端 SDK 始终与最新服务端代码一致,避免文档、TS SDK、Dart SDK 三者漂移。

小结

Immich 的 API 体系可以归纳为一条清晰的单向数据流:

NestJS 控制器 + @nestjs/swagger 装饰器
        │  SwaggerModule.createDocument / patchOpenAPI
        ▼
open-api/immich-openapi-specs.json   (191 个路径,/api 前缀)
        │  oazapfts / openapi-generator(mise open-api 编排)
        ▼
packages/sdk/src/fetch-client.ts(TypeScript)  +  mobile/generated/openapi(Dart)
        │  + 手写封装层 index.ts / 模板补丁与 patch 文件
        ▼
Web 前端、CLI、移动端共用的强类型客户端

理解这条链路的实际收益是:任何关于“API 文档和 SDK 不同步”的问题,都可以沿 server → specs.json → 生成器 逐层定位;而所有定制行为都被约束在模板补丁与 patch 文件中,使生成流程保持幂等可重放。

登录后查看全文
热门项目推荐
相关项目推荐