mcpField
D
data-client-rest-setup

data-client-rest-setup

github.com/reactive开发2.0k36 次安装今天更新
代码仓库

关于 data-client-rest-setup

用于在 REST API 项目中配置 `@data-client/rest` 的设置与迁移技能。它识别现有 HTTP 库、引导迁移,并协助设计项目专用的 endpoint 基类。

主要功能

  1. 01识别现有 HTTP 库
  2. 02引导从 Axios、fetch、Ky、SuperAgent 和 Got 迁移
  3. 03配置自定义 RestEndpoint 基类
  4. 04提供 resource、endpoint 和 schema 建议

适用场景

  1. 01将应用从 Axios 或 fetch 迁移
  2. 02为新项目添加 REST 支持
  3. 03统一 endpoint 的认证与响应处理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/reactive/data-client --skill data-client-rest-setup

安装后在 Claude Code 中输入/data-client-rest-setup确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
axios-migration.md
fetch-migration.md
got-migration.md
ky-migration.md
superagent-migration.md

详细介绍

REST 协议设置与迁移

此技能为项目配置 @data-client/rest,可用于全新设置,也可用于从现有 HTTP 库迁移。应在技能「data-client-setup」检测到 REST API 模式后应用。

请先应用技能「data-client-rest」,以确保实现模式准确。

第 1 步:安装

将 REST 包与核心包一同安装:

# npm
npm install @data-client/rest

# yarn
yarn add @data-client/rest

# pnpm
pnpm add @data-client/rest

第 2 步:检测现有 HTTP 模式

扫描代码库,确定当前使用的库。多种模式可能并存——针对相关文件,分别运行每个适用的迁移子流程。

检测清单

检查 package.json 中的依赖项,并扫描源文件:

检查模式操作
依赖项中有 "axios",或源文件中有 import.*from ['"]axios['"]Axios按照 references/axios-migration.md 操作
有使用 REST 风格 URL 的 fetch( 调用,或有封装 fetch 的函数原生 fetch按照 references/fetch-migration.md 操作
依赖项中有 "ky",或有 import.*from ['"]ky['"]Ky按照 references/ky-migration.md 操作
依赖项中有 "superagent"SuperAgent按照 references/superagent-migration.md 操作
依赖项中有 "got"(在浏览器代码中较少见)Got按照 references/got-migration.md 操作
未检测到现有 HTTP 库全新项目跳至第 3 步:自定义 RestEndpoint 基类
检测结果不明确

如果无法确定使用了哪些模式(例如,没有明确的导入语句,但存在 HTTP 调用),请询问用户:

我在你的代码库中发现了 HTTP 调用,但无法确定所用的库。你是要从以下哪种方案迁移?

  1. axios
  2. 原生 fetch / 自定义 fetch 封装
  3. ky
  4. superagent
  5. 其他方案(请说明)
  6. 从头开始(无需迁移)
混合代码库

检测到多个 HTTP 库时,针对相关文件分别运行各个子流程。这些子流程彼此独立,不会冲突:

  1. 确定哪些文件使用了哪些库(按导入语句分组)
  2. 对每组文件运行对应的迁移子流程
  3. 完成所有迁移后,继续设置基类

迁移参考

每项迁移都有独立的参考文档。根据上面的检测结果,只阅读相关文档。完成迁移后,返回此处设置基类。

第 3 步:自定义 RestEndpoint 基类

完成安装和所有迁移后,请主动提出为项目创建自定义 RestEndpoint 类。

检测清单

扫描现有代码库,查找常见的 REST 模式,包括:

  1. 基础 URL / API 前缀:查找硬编码的 URL,例如 https://api.example.com,或环境变量,例如 process.env.API_URL
  2. 身份验证:查找 Authorization 标头、localStorage/cookies 中的令牌、身份验证拦截器
  3. Content-Type 处理:检查 API 是否使用 JSON、form-data 或自定义内容类型
  4. 错误处理:查找错误响应模式、状态码处理
  5. 请求/响应转换:数据转换、日期解析、大小写转换
  6. 查询字符串格式:简单参数与嵌套对象(可能需要 qs 库)
基类模板

在 src/api/BaseEndpoint.ts 创建文件(或根据项目结构选择类似位置):

import { RestEndpoint, RestGenerics } from '@data-client/rest';

/**
 * Base RestEndpoint with project-specific defaults.
 * Extend this for all REST API endpoints.
 */
export class BaseEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
  // API base URL - adjust based on detected patterns
  urlPrefix = process.env.REACT_APP_API_URL ?? 'https://api.example.com';

  // Add authentication headers
  getHeaders(headers: HeadersInit): HeadersInit {
    const token = localStorage.getItem('authToken');
    return {
      ...headers,
      ...(token && { Authorization: `Bearer ${token}` }),
    };
  }
}

常见生命周期重写

根据代码库中检测到的情况添加这些内容。完整 API 文档请参阅 RestEndpoint。

身份验证(异步刷新令牌)
async getHeaders(headers: HeadersInit): Promise<HeadersInit> {
  const token = await getValidToken(); // handles refresh
  return {
    ...headers,
    Authorization: `Bearer ${token}`,
  };
}
来自 React context 的身份验证(Okta、Auth0)

当身份验证令牌存储在 React context 中(而非 localStorage)时,基类上的 getHeaders() 无法访问这些令牌。使用 hookifyResource() 将从 context 获取的标头注入每个端点:

import { hookifyResource, resource } from '@data-client/rest';

const ArticleResourceBase = resource({
  path: '/articles/:id',
  schema: Article,
  Endpoint: BaseEndpoint,
});

export const ArticleResource = hookifyResource(
  ArticleResourceBase,
  function useInit() {
    const accessToken = useContext(AuthContext);
    return {
      headers: { Authorization: `Bearer ${accessToken}` },
    };
  },
);

用法:useSuspense(ArticleResource.useGet(), { id }) — 该 Hook 会在每次渲染时调用 useInit(),因此令牌始终是从 context 获取的最新令牌。

自定义请求初始化(CSRF、凭据)
getRequestInit(body?: RequestInit['body'] | Record<string, unknown>): RequestInit {
  return {
    ...super.getRequestInit(body),
    credentials: 'include', // for cookies
    headers: {
      'X-CSRF-Token': getCsrfToken(),
    },
  };
}
自定义响应解析(解包数据信封)
process(value: any, ...args: any[]) {
  // If API wraps responses in { data: ... }
  return value.data ?? value;
}
自定义错误处理
async fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response> {
  const response = await super.fetchResponse(input, init);
  
  if (response.status === 401) {
    window.dispatchEvent(new CustomEvent('auth:expired'));
  }
  
  return response;
}
自定义搜索参数(使用 qs 库)
searchToString(searchParams: Record<string, any>): string {
  return qs.stringify(searchParams, { arrayFormat: 'brackets' });
}
自定义 parseResponse(处理非 JSON 响应)
async parseResponse(response: Response): Promise<any> {
  const contentType = response.headers.get('content-type');
  
  if (contentType?.includes('text/csv')) {
    return parseCSV(await response.text());
  }
  
  return super.parseResponse(response);
}

包含多个重写项的完整示例

import { RestEndpoint, RestGenerics } from '@data-client/rest';
import qs from 'qs';

export class BaseEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
  urlPrefix = process.env.API_URL ?? 'http://localhost:3001/api';

  async getHeaders(headers: HeadersInit): Promise<HeadersInit> {
    const token = await getAuthToken();
    return {
      ...headers,
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
    };
  }

  getRequestInit(body?: RequestInit['body'] | Record<string, unknown>): RequestInit {
    return {
      ...super.getRequestInit(body),
      credentials: 'include',
    };
  }

  searchToString(searchParams: Record<string, any>): string {
    return qs.stringify(searchParams, { arrayFormat: 'brackets' });
  }

  process(value: any, ...args: any[]) {
    return value?.data ?? value;
  }
}

async function getAuthToken(): Promise<string | null> {
  return localStorage.getItem('token');
}

设置后的用法

创建基类后,使用它,而不是直接使用 RestEndpoint。

选择 resource() 还是单独的端点

当 API 模块在单个路径上提供标准 CRUD 操作(列表、获取、创建、更新、删除)时,使用 resource()。这是常见情况:

import { resource } from '@data-client/rest';
import { BaseEndpoint } from './BaseEndpoint';
import { Todo } from '../schemas/Todo';

export const TodoResource = resource({
  path: '/todos/:id',
  schema: Todo,
  Endpoint: BaseEndpoint,
});
// Provides: TodoResource.get, .getList, .create, .update, .delete, .partialUpdate

对于非 CRUD 操作(搜索、身份验证、自定义操作),或者路径不符合 resource() 约定时,使用独立的 new BaseEndpoint():

export const loginEndpoint = new BaseEndpoint({
  path: '/auth/login',
  method: 'POST' as const,
  body: {} as { email: string; password: string },
  schema: undefined,
});

请求体类型:使用 body: {} as BodyType(真值)——不要使用 undefined as unknown as BodyType。需要提供真值,端点才能正确为 POST/PUT/PATCH 发送请求体。

与现有验证共存(Zod、Yup)

如果代码库已使用 Zod/Yup 验证响应,优先将 Entity 作为事实来源,用于受益于缓存/规范化的类型。仅对不需要规范化的类型保留 Zod(身份验证令牌、表单验证类型、一次性响应)。详细选项请参阅迁移参考文件。

后续步骤

  1. 定义 Entity 类(技能“data-client-schema”),并通过 schema: 将其连接到端点——这一步至关重要,并非可选项。设置了 schema: undefined 的端点会绕过规范化和缓存。
  2. 应用技能“data-client-rest”,了解资源和端点模式
  3. 应用技能“data-client-react”或“data-client-vue”,了解基于 Hook 的用法

参考资料

评论

支持 Markdown