名称: api-organization 描述: 解释此代码库中标准化的API组织模式。在创建新的API端点、API客户端或修改现有API结构时使用。涵盖5文件系统(端点类型、端点、API客户端、管理员API客户端、受保护端点)、基于角色的访问模式(管理员与普通用户)以及API层的TypeScript类型安全。所有API代码都位于src/lib/api/中,遵循此确切模式。
API组织模式
此技能定义了在整个代码库中使用的标准化API组织模式。所有外部API集成都遵循相同的5文件结构,以确保一致性、类型安全性和可维护性。
何时使用此技能
在以下情况下使用此技能:
- 创建新的API端点或集成
- 添加新的API类别或领域
- 实现基于角色的API访问(管理员与普通用户)
- 修改现有API结构
- 设置API调用的身份验证
- 创建类型安全的API包装器
核心原则
- 单一事实来源 - 所有API URL在
endpoints.ts中定义一次 - 类型安全 - 从参数到响应的完整TypeScript覆盖
- 基于角色的访问 - 普通端点和管理员端点的独立客户端
- 集中式身份验证 - 客户端自动处理身份验证
- DRY代码 - 可重用的客户端函数,无重复的端点定义
5文件系统
所有API代码都位于src/lib/api/中,包含以下确切文件:
src/lib/api/
├── endpoint-types.ts # 所有端点的TypeScript类型
├── endpoints.ts # 按领域组织的URL定义
├── api-client.ts # 通用身份验证API客户端
├── admin-api-client.ts # 仅限管理员的API客户端,带角色检查
└── protected-endpoints.ts # 类型安全的包装器函数
文件用途
1. endpoint-types.ts
- 定义API数据的所有TypeScript接口
- 按领域组织类型(例如,受众、实例、成员资格)
- 包括参数类型、响应类型和请求体DTO
- 提供用于提取类型的实用程序类型
详细结构请参见references/endpoint-types-pattern.md。
2. endpoints.ts
- 在一个位置定义所有API端点URL
- 按与类型匹配的功能/领域组织
- 使用接受参数并返回URL的函数
- 支持环境变量基础URL
详细结构请参见references/endpoints-pattern.md。
3. api-client.ts
- 具有自动身份验证的通用API客户端
- 错误解析和处理
- 支持GET、POST、PUT、DELETE方法
- 仅限服务器端(‘use server’)
实现请参见references/api-client-pattern.md。
4. admin-api-client.ts
- 管理员特定的API客户端
- 角色验证(管理员/超级管理员组)
- 权限检查函数
- 如果未授权则抛出AdminAuthError
实现请参见references/admin-api-client-pattern.md。
5. protected-endpoints.ts
- 结合URL + 类型 + 客户端的类型安全包装器函数
- 组织以匹配endpoint-types结构
- 提供干净的导入:
import { api } from '@/lib/api/protected-endpoints' - 无’use server’指令(导出对象)
详细结构请参见references/protected-endpoints-pattern.md。
身份验证系统
此应用程序使用Supabase Auth进行身份验证。所有API请求都需要通过Supabase访问令牌进行身份验证。
Supabase客户端结构
src/lib/supabase/
├── client.ts # 浏览器端Supabase客户端
├── server.ts # 服务器端Supabase客户端(RSC、服务器操作)
└── middleware.ts # 用于身份验证Cookie刷新的中间件助手
身份验证流程
- 用户通过Supabase进行身份验证(电子邮件/密码、OAuth等)
- Supabase将会话存储在httpOnly Cookie中
- 中间件在每个请求上刷新会话
- 服务器组件/操作使用
server.ts中的createClient() - API客户端自动从会话中提取访问令牌
详细实现请参见references/supabase-auth-integration.md。
基于角色的访问模式
普通用户端点
使用具有自动Supabase身份验证的api-client.ts函数:
import { apiGet, apiPost } from '@/lib/api/api-client';
// 自动从Supabase会话中提取访问令牌
const data = await apiGet<ResponseType>(url);
仅限管理员端点
使用具有角色验证的admin-api-client.ts函数:
import { adminApiRequest, checkAdminPermission } from '@/lib/api/admin-api-client';
// 首先验证管理员访问权限(从数据库检查用户角色)
await checkAdminPermission(); // 如果不是管理员则抛出错误
// 发出管理员API请求
const data = await adminApiRequest<ResponseType>(url, options);
管理员角色
管理员角色存储在users表中:
users.role='admin'- 标准管理员访问权限users.role='member'- 普通用户访问权限
家庭中的第一个用户自动分配管理员角色。
添加新的API端点
将新的API集成到应用程序时,请遵循以下确切顺序:
步骤1:在endpoint-types.ts中定义类型
// 1. 定义响应类型
导出接口 ResourceItem {
id: string;
name: string;
// ... 来自API响应的其他字段
}
// 2. 为变更定义请求DTO
导出接口 CreateResourceDto {
name: string;
// ... 创建所需的字段
}
// 3. 将参数类型添加到EndpointParams接口
导出接口 EndpointParams {
// ... 现有领域
resources: {
list: void; // 不需要参数
get: { id: string }; // 需要ID
create: void; // 请求体中的内容,非参数
update: { id: string };
delete: { id: string };
};
}
// 4. 将响应类型添加到EndpointResponses接口
导出接口 EndpointResponses {
// ... 现有领域
resources: {
list: ResourceItem[];
get: ResourceItem;
create: ResourceItem;
update: ResourceItem;
delete: void;
};
}
// 5. 将请求体类型添加到EndpointBodies接口(如果需要)
导出接口 EndpointBodies {
// ... 现有领域
resources: {
create: CreateResourceDto;
update: CreateResourceDto;
};
}
步骤2:在endpoints.ts中定义URL
导出 const API_ENDPOINTS = {
// ... 现有类别
resources: {
list: () => `${API_BASE}/api/resources`,
get: (id: string) => `${API_BASE}/api/resources/${id}`,
create: () => `${API_BASE}/api/resources`,
update: (id: string) => `${API_BASE}/api/resources/${id}`,
delete: (id: string) => `${API_BASE}/api/resources/${id}`,
},
};
步骤3:在protected-endpoints.ts中添加包装器
导出 const api = {
// ... 现有领域
resources: {
async list(): Promise<ResourceItem[]> {
return apiGet<ResourceItem[]>(
API_ENDPOINTS.resources.list()
);
},
async get(id: string): Promise<ResourceItem> {
return apiGet<ResourceItem>(
API_ENDPOINTS.resources.get(id)
);
},
async create(data: CreateResourceDto): Promise<ResourceItem> {
return apiPost<ResourceItem, CreateResourceDto>(
API_ENDPOINTS.resources.create(),
data
);
},
async update(id: string, data: CreateResourceDto): Promise<ResourceItem> {
return apiPut<ResourceItem, CreateResourceDto>(
API_ENDPOINTS.resources.update(id),
data
);
},
async delete(id: string): Promise<void> {
return apiDelete<void>(
API_ENDPOINTS.resources.delete(id)
);
},
},
};
步骤4:在应用程序代码中使用
'use server';
import { api } from '@/lib/api/protected-endpoints';
// 在服务器组件或服务器操作中
const resources = await api.resources.list();
const resource = await api.resources.get(id);
const newResource = await api.resources.create({
name: '新资源'
});
命名约定
端点操作
list/listAll- 获取多个项目get- 获取单个项目create- 创建新项目update- 更新现有项目delete- 删除项目
类型命名
- 响应类型:描述实体的帕斯卡命名法(例如,
AudienceListItem) - DTO:带后缀的帕斯卡命名法(例如,
CreateUserDto、UpdateSettingsDto) - 接口匹配集合的复数形式,项目的单数形式
错误处理
所有API客户端自动处理错误:
try {
const data = await api.myFeature.get(id);
} catch (error) {
// 错误已解析并格式化
console.error('API错误:', error.message);
}
常见错误类型:
AdminAuthError- 管理员权限被拒绝AdminApiError- 管理员API请求失败- 来自
api-client的通用错误,带有解析后的消息
身份验证流程
普通端点
- 客户端调用受保护的端点包装器(例如,
api.resources.list()) - 包装器调用api-client中的apiGet/apiPost等
- api-client调用
getAuthHeaders() - getAuthHeaders使用Supabase服务器客户端获取会话:
const supabase = await createClient(); const { data: { session } } = await supabase.auth.getSession(); const accessToken = session?.access_token; - 访问令牌添加到Authorization标头
- 请求发送时自动进行身份验证
管理员端点
与上述流程相同,但带有额外的角色检查:
- 首先调用
checkAdminPermission() - checkAdminPermission查询
users表以获取当前用户的角色 - 如果角色不是’admin’,则抛出
AdminAuthError - 然后继续进行正常的身份验证流程
最佳实践
- 切勿硬编码URL - 始终使用
API_ENDPOINTS - 始终先定义类型 - 类型驱动实现
- 每个端点一个包装器 - 保持protected-endpoints整洁
- 按领域分组 - 在所有5个文件中匹配结构
- 使用辅助函数 -
apiGet、apiPost等处理身份验证 - 尽早验证管理员访问权限 - 首先调用
checkAdminPermission() - 记录复杂端点 - 添加JSDoc注释
- 优雅地处理错误 - API客户端提供良好的错误消息
环境变量
Supabase身份验证所需
NEXT_PUBLIC_SUPABASE_URL- Supabase项目URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY- Supabase匿名公钥
API客户端所需
INSTANCE_API_URL或API_URL- 外部API端点的基础URLNEXT_PUBLIC_SITE_URL- 站点URL用于重定向(可选,默认为localhost:3000)
数据库
- Supabase连接通过Supabase客户端自动处理
- 无需手动连接字符串
从其他模式迁移
如果将现有API代码迁移到此模式:
- 将所有端点URL提取到
endpoints.ts - 在
endpoint-types.ts中为参数/响应创建类型 - 将直接fetch调用替换为
apiGet/apiPost等 - 将包装器添加到
protected-endpoints.ts - 更新导入以使用
api对象
迁移示例:
// 之前(分散的fetch调用)
const response = await fetch(`${API_BASE}/api/resources/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
const resource = await response.json();
// 之后(集中式模式)
import { api } from '@/lib/api/protected-endpoints';
const resource = await api.resources.get(id); // 身份验证自动,包含类型
参考
详细实现模式请参见参考文件:
references/supabase-auth-integration.md- Supabase身份验证设置和集成references/endpoint-types-pattern.md- 类型定义结构references/endpoints-pattern.md- URL组织模式references/api-client-pattern.md- 带有Supabase的通用客户端实现references/admin-api-client-pattern.md- 具有基于角色访问权限的管理员客户端references/protected-endpoints-pattern.md- 包装器函数模式