# Fumadocs (Framework Mode, TanStack Start): createOpenAPI()
URL: /zh-TW/docs/tanstack-start/integrations/openapi/server
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/openapi/server.zh-TW.mdx

OpenAPI 伺服器實例。

## OpenAPI 伺服器 [#openapi-server]

Fumadocs OpenAPI 的主要設定。

> 請勿在瀏覽器環境中引用此設定。

### `input` [#input]

要讀取的 OpenAPI schema。

* 檔案路徑
* 外部 URL
* 函式 (見下文) 

只有列在此處的 schema 才能載入；若傳入 `getSchema()` 或由頁面預先載入的是其他 ID，則會拋出錯誤。

<Callout type="warn">
請勿將不受信任的 schema 傳入 `input`，因為其中的 `$ref` 可以讀取本機檔案並擷取 URL，私有主機也包含在內。
</Callout>

```ts tab="Basic"
import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  input: ['./unkey.json'],
});
```

```ts tab="Functions"
import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  input: {
    // [id]：已下載的 OpenAPI schema
    my_schema: async () => {
      const res = await fetch('https://registry.scalar.com/@scalar/apis/galaxy/latest?format=json');
      return res.json();
    },
  },
});
```

## 只載入一次文件 [#loading-documents-once]

頁面會透過 props 接收完整的文件，因此每次載入頁面、切換頁面與預先擷取時，都會重新傳送整份文件。若文件較大，建議將其排除：

```ts title="lib/openapi.ts"
import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  // ID 最後會出現在 URL 中，請確保可安全用於 URL
  input: { museum: './museum.yaml' },
  includeDocument: false, // [!code ++]
});
```

TanStack Start 的路由載入器會同時在伺服器與瀏覽器上執行，因此您的文件路由不需要 `loadDocument` 就能載入文件。只有在瀏覽器尚未取得文件時，才需要呼叫 server function：

```tsx title="src/routes/docs/$.tsx"
import { createServerFn } from '@tanstack/react-start';
import type { Document } from 'fumadocs-openapi';
import { openapi } from '@/lib/openapi';

// 瀏覽器已載入的文件
const documents = new Map<string, Document>();

const getDocument = createServerFn({ method: 'GET' })
  .validator((id: string) => id)
  .handler(async ({ data: id }) => (await openapi.getSchema(id)).bundled);

export const Route = createFileRoute('/docs/$')({
  component: Page,
  loader: async ({ params }) => {
    const data = await serverLoader({ data: params._splat?.split('/') ?? [] });
    if (data.type === 'openapi') {
      const { document: id, payload } = data.props;
      payload.bundled = documents.get(id) ?? (await getDocument({ data: id }));
    }
    return data;
  },
});

function Page() {
  const page = useFumadocsLoader(Route.useLoaderData());
  // 第一個頁面的文件也一併記錄
  if (page.type === 'openapi' && typeof window !== 'undefined')
    documents.set(page.props.document, page.props.payload.bundled!);
  // ...
}
```

* 若使用產生的 MDX 檔案，請改將文件放入 `preloaded.docs`。
* 瀏覽器會保留每份文件，直到重新載入頁面為止。使用 `disableCache` 時，請保持啟用 `includeDocument`。

## 建立代理伺服器 [#creating-proxy]

代理伺服器傳送 HTTP 請求時不受瀏覽器的 CORS 限制。API 互動測試區會在以下情況使用它：

* 請求未透過 CORS 允許您的來源的 API 端點。
* 機密用戶端 (具有 client secret) 的 OAuth 請求，以及裝置授權流程。

<Callout type="warn">
代理伺服器是公開的，無須驗證即可存取。它會轉送請求的標頭與 body，包括 `Authorization`。請使用 [`allowedOrigins`](#allowed-origins) 與 `filterRequest` 限制其存取。
</Callout>

### 設定 [#setup]

為代理伺服器建立路由處理常式：

```ts title="src/routes/api/proxy.ts"
import { openapi } from '@/lib/openapi';
import { createFileRoute } from '@tanstack/react-router';

const proxy = openapi.createProxy();

export const Route = createFileRoute('/api/proxy')({
  server: {
    handlers: {
      // 預設處理所有方法，也可以個別指定方法（GET、POST 等）
      ANY: async ({ request }) => {
        return proxy.handle(request);
      },
    },
  },
});
```

接著在 `createOpenAPI()` 中設定其 URL：

```ts title="lib/openapi.ts"
import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  proxyUrl: '/api/proxy', // [!code ++]
});
```

### 允許的來源 [#allowed-origins]

代理伺服器預設會接受任何公開來源，並拒絕私有網路上的主機 (例如 `localhost`) ，也不會轉送您網站的 Cookie。
若要限制存取範圍，或要代理私有 API，請在代理伺服器路由中列出允許的來源：

```ts
openapi.createProxy({
  // 字串或正規表示式
  allowedOrigins: ['https://api.example.com', 'http://localhost:8080'],
});
```

重新導向也會依照相同的規則進行檢查。

<Callout type="warn">
封鎖私有網路的功能需要 Node.js 或 Deno。若使用其他執行階段 (例如 Bun) ，請指定
`allowedOrigins`。
</Callout>

您可以使用 `filterRequest` 拒絕請求，例如只允許讀取操作：

```ts
openapi.createProxy({
  // 回傳 false 的代理請求會被拒絕
  filterRequest: (request) => request.method === 'GET',
});
```

## OAuth 重新導向 [#oauth-redirect]

OAuth 提供者只接受已註冊的重新導向 URI，但 API 互動測試區預設會重新導向至目前頁面。
請建立一個路由處理常式，作為所有頁面共用的重新導向 URI。它會將使用者導回發起流程的頁面。

```ts title="src/routes/api/oauth.ts"
import { openapi } from '@/lib/openapi';
import { createFileRoute } from '@tanstack/react-router';

const handler = openapi.createOAuthHandler();

export const Route = createFileRoute('/api/oauth')({
  server: {
    handlers: {
      GET: async ({ request }) => handler(request),
    },
  },
});
```

請將此路由的 URL 傳入 `createOpenAPIPage()`，並在 OAuth 提供者中註冊完整 URL (例如 `https://docs.example.com/api/oauth`) ：

```tsx title="components/api-page.tsx"
export const OpenAPIPage = createOpenAPIPage({
  oauthRedirectUrl: '/api/oauth', // [!code ++]
});
```

<Callout title="內容安全政策（Content Security Policy）">
處理常式會透過其自身路由提供的指令碼將使用者導回。若該路由受 Content Security Policy 規範，該政策必須允許 `'self'` 指令碼。使用 `'strict-dynamic'` 的政策會忽略 `'self'`，因此請改為將該路由排除在這類政策之外。
</Callout>

### 靜態網站 [#static-sites]

在靜態網站中，路由會預先算繪為檔案。請傳入 `inline: true` 將指令碼內嵌於頁面中，並讓路徑以 `.html` 結尾，讓靜態主機將該檔案視為 HTML 提供。

```ts tab="src/routes/api/oauth[.]html.ts"
import { openapi } from '@/lib/openapi';
import { createFileRoute } from '@tanstack/react-router';

const handler = openapi.createOAuthHandler({ inline: true });

export const Route = createFileRoute('/api/oauth.html')({
  server: {
    handlers: {
      GET: async ({ request }) => handler(request),
    },
  },
});
```

```ts tab="vite.config.ts"
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: { enabled: true },
      // 沒有任何頁面連結到此路由，因此將它與預設的爬取起點一併列出
      pages: [{ path: '/' }, { path: '/api/oauth.html' }], // [!code ++]
    }),
    // ...
  ],
});
```

請使用 `.html` URL 作為重新導向 URI，例如 `oauthRedirectUrl: '/api/oauth.html'`。由於指令碼內嵌於頁面中，套用於此路由的內容安全政策 (Content Security Policy) 必須允許內嵌指令碼。