Skip to main content

elf-api-design

Elf Express RESTful API 設計規範(ASP.NET Core 10 Minimal API),規定 URL 命名、HTTP method 與狀態碼、 endpoint → service → db 分層、ProblemDetails 錯誤格式、分頁/排序/篩選參數、版本控制、Bearer 認證、CORS、 rate limiting、OpenAPI 與 /api/health。 當任務涉及新增或修改 API endpoint、命名路由、決定回傳狀態碼、設計錯誤回應、分頁查詢、 API 版本 (v1/v2)、Authorization header、CORS 設定、限流、OpenAPI/Swagger 或健康檢查時觸發。

来源信息

仓库
elf-express/skillshare-source
最近来源活动
2026年9月27日 01:26
检测到的 SKILL.md 语言
中文
星标
0
Forks
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
elf-api-design
description
Elf Express RESTful API 設計規範(ASP.NET Core 10 Minimal API),規定 URL 命名、HTTP method 與狀態碼、 endpoint → service → db 分層、ProblemDetails 錯誤格式、分頁/排序/篩選參數、版本控制、Bearer 認證、CORS、 rate limiting、OpenAPI 與 /api/health。 當任務涉及新增或修改 API endpoint、命名路由、決定回傳狀態碼、設計錯誤回應、分頁查詢、 API 版本 (v1/v2)、Authorization header、CORS 設定、限流、OpenAPI/Swagger 或健康檢查時觸發。
metadata
{"version":"1.1.0","owner":"Elf Express"}
# Elf Express RESTful API 設計規範 後端技術棧:ASP.NET Core 10 **Minimal API** + SqlSugar 5.x + PostgreSQL 18。 本規範描述「API 表面長什麼樣子」。前後端如何保持同步(`docs/api-contract.md`、`types.ts`、mock)請看 **elf-api-contract**; C# 分層、DI、設定請看 **elf-dotnet**;查詢與分頁的 SqlSugar 寫法請看 **elf-sqlsugar**; 端對端 HTTP 測試請看 **elf-integration**;容器與 healthcheck 請看 **elf-cicd-docker**;PR 審查規則請看 **elf-cicd-review**。 > 標示「**新專案 MUST(參考專案未實作)**」的規則是 ASP.NET Core 10 標準做法,參考專案尚未採用;新專案一律照做,舊專案修改到該處時一併導入。 > 目錄慣例:API 專案在 `server/src/<App>.Api/`,測試在 `server/tests/<Project>.Tests/`;HTTP / 契約測試在 `server/tests/<App>.Api.IntegrationTests/`(見 elf-integration)。 --- ## 1. 何時使用 - 新增、修改、刪除任何 HTTP endpoint。 - 決定 URL、HTTP method、狀態碼、查詢參數名稱。 - 撰寫或修改錯誤處理、`Program.cs` 的 middleware 管線(forwarded headers、例外、CORS、認證、限流、OpenAPI、health)。 - Code review 時檢查 API 是否符合團隊慣例。 --- ## 2. 固定規則 ### 2.1 路由與命名 1. **MUST** 所有 endpoint 掛在 `app.MapGroup("/api")` 底下。前端 `VITE_API_BASE_URL=/api`、nginx `location /api/` 都依賴這個前綴。 2. **MUST** 每個功能一個 `<Feature>Endpoints` 靜態類別,對外只暴露一個擴充方法: `public static RouteGroupBuilder Map<Feature>(this IEndpointRouteBuilder app, string prefix = "/<resources>")`, 在 `Program.cs` 以 `api.Map<Feature>();` 註冊。檔案放 `server/src/<App>.Api/Api/<Feature>Endpoints.cs`。 分層固定 **endpoint → service → db**:endpoint 只做綁定、格式驗證、呼叫 `I<Feature>Service`(`server/src/<App>.Api/Services/<Feature>Service.cs`,Scoped)、把結果對應成狀態碼; **MUST NOT** 在 endpoint 注入 `ISqlSugarClient`。每個 handler **MUST** 宣告 `CancellationToken ct` 並一路傳到 service 與 SqlSugar(`FirstAsync(ct)`、`ToPageListAsync(..., ct)`)。 3. **MUST** 資源用**複數名詞、全小寫**:`/categories`、`/texts`、`/words`、`/sessions`。多字用 **kebab-case**:`/error-books`。**MUST NOT** 用 `camelCase`、`snake_case` 或動詞(`/getCategories`)。 4. **MUST** 路由參數加型別約束:`/{id:int}`、`/{id:guid}`;以代碼識別的資源用 `/{code}`。 5. **MUST** 目前登入者自己的資料用 `/<resources>/me`(例:`GET /sessions/me`)。**MUST NOT** 讓前端在路由或 query 傳自己的 `userId`,一律從 JWT claims(`ElfClaimTypes.UserId`)取。 6. **MUST** 子資源最多巢狀一層:`/models/{code}/install`。更深的關係改用 query:`/texts?category=basic`。 7. **MUST** 非 CRUD 的動作依下列判斷選路由: - 動作結果是「某個狀態存在 / 不存在」、事後可以 `GET` 查到(安裝、訂閱、釘選、啟用)→ 建模成**子資源**,`POST` 建立、`DELETE` 移除:`POST /models/{code}/install`、`DELETE /models/{code}/install`。 - 動作是一次性計算或批次處理、不留下可單獨 `GET` 的狀態(翻譯、匯入、重算)→ 才用**動作路由**,且 **MUST** 用 `POST`:`POST /translate/locales`、`POST /words/import`。 8. **MUST** Query 參數用 **camelCase**:`?category=&level=`、`?lang=en&days=30`、`?locale=zh-TW`。 9. **MUST** 每個 endpoint 呼叫 `.WithName("<Verb><Noun>")`(例 `GetModels`、`TranslateLocales`),每個 group 呼叫 `.WithTags("<Feature>")`,OpenAPI 才有穩定的 operationId 與分組。 10. **MUST NOT** 回傳 SqlSugar Entity,一律投影成 `<Name>Dto` record。**MUST NOT** 在新專案使用泛型 `MapCrud<T>`;既有專案保留的 `MapCrud<T>` 只准用在「沒有敏感欄位、純後台維護」的表,**MUST NOT** 用在含 `PasswordHash`、token、個資的 Entity,且必須符合規則 11、14(`Location` 含 `/api`、PUT 以路由 id 為準)。 ### 2.2 HTTP method 與狀態碼 11. **MUST** 依下表回應。參考專案 `Api/CrudEndpoints.cs` 只有部分符合(PUT 忽略路由 id、`Location` 少 `/api`),**MUST NOT** 以它為範本: | 操作 | Method | 成功 | 常見失敗 | |---|---|---|---| | 列表 | `GET /x` | `200` + `T[]` 或 `PagedResult<T>` | `400` 參數錯 | | 取單筆 | `GET /x/{id}` | `200` + DTO | `404` | | 建立 | `POST /x` | `201 Created`,`Location: /api/x/{id}`,body 為完整 DTO | `400`、`409` 重複 | | 整筆更新 | `PUT /x/{id}` | `204 No Content` | `400`、`404` | | 部分更新 | `PATCH /x/{id}` | `204` | `400`、`404` | | 刪除 | `DELETE /x/{id}` | `204` | `404` | | 子資源動作 | `POST`/`DELETE /x/{id}/<action>` | `204`(無回傳)或 `200` + 結果 | `404`、`409` | | 計算型 POST | `POST /translate/locales` | `200` + 結果 DTO | `400` | 12. **MUST** 未登入或 token 無效回 `401`(前端收到會清掉 token),已登入但沒權限回 `403`。**MUST NOT** 用 `200` + `success:false` 表示錯誤。 13. **MUST** 驗證失敗回 `400`(`ValidationProblem`),業務衝突(重複代碼、狀態不允許)回 `409`,被限流回 `429`,未預期例外回 `500`。 14. **MUST** `PUT /x/{id}` 以路由的 `id` 為準:service 先依路由 id 讀出再套用 body;body 若也帶 id 且與路由不同,回 `400`。**MUST NOT** 直接 `Updateable(body)`。 15. **MUST** `GET`、`PUT`、`DELETE` 為冪等;`GET` **MUST NOT** 有副作用。 ### 2.3 錯誤回應(ProblemDetails) 16. **MUST** 所有錯誤回應為 RFC 9457 **ProblemDetails**(`Content-Type: application/problem+json`)加 `code`,形狀固定: ```json { "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5", "title": "Not Found", "status": 404, "detail": "Category 42 was not found.", "instance": "/api/categories/42", "code": "category.not_found", "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" } ``` - `detail`:給人看的英文句子。 - `code`:機器可判斷的錯誤碼,格式 `<resource>.<reason>`,全小寫 snake_case(`category.not_found`、`session.score_mismatch`、`auth.invalid_credentials`)。前端要依錯誤分流時**只能**判斷 `code`,不可比對 `detail` 字串。所有 `code` 列在 `docs/api-contract.md` 的「Error codes」表(elf-api-contract 規則 7)。 - 驗證錯誤另有 `errors: { "<camelCaseField>": ["message"] }`,`code` 固定 `validation.failed`。 17. **MUST** 錯誤處理分工如下(與 elf-dotnet 相同): - **service** 對可預期的失敗回傳**結果 enum**(寫入類,例 `CategoryWriteResult.DuplicateCode`)或 `T?`(查詢類,`null` = 找不到)。service **MUST NOT** throw `AppException`、**MUST NOT** 回傳 `IResult`。 - **endpoint** 把結果對應成 `Problems.NotFound/Conflict/BadRequest/Forbidden(code, detail)`(內部為 `TypedResults.Problem` + `code` extension)或 `ApiValidation.Problem(...)`。`AppException` 只允許在 endpoint 層、無法回傳 `IResult` 的 helper 中使用。 - `Program.cs` 啟用 `AddProblemDetails()`、`AddExceptionHandler<GlobalExceptionHandler>()`、`app.UseExceptionHandler()`、`app.UseStatusCodePages()`,未預期例外由全域 handler 轉成 `500`。 - 參考專案目前回 `Results.BadRequest(new { message = "..." })`;新程式碼 **MUST NOT** 再寫 `{ message }` 形狀。 18. **MUST NOT** 把 exception message、stack trace、SQL 回傳給前端。`500` 的 `detail` 固定為 `"An unexpected error occurred."`、`code` 為 `server.error`,細節只寫 log(帶 `traceId`)。 19. **MUST** 前端 `normalizeError` 依序讀 `detail → title → message`(`message` 相容舊回應),見 elf-api-contract。 完整程式碼:[`references/server-templates.md` §1](references/server-templates.md#1-錯誤處理)。 ### 2.4 分頁、排序、篩選 20. **MUST** 任何**可能超過 500 列**(= `size` 上限)的列表一律分頁,包括紀錄、歷史、使用者產生的資料;只有能確定永遠 ≤ 500 列的目錄型資料(分類、內建 prompt、模型清單)可回傳裸陣列 `T[]`。無法確定時就分頁。 21. **MUST** 分頁參數固定為 `?page=1&size=50`:`page` 從 **1** 開始、`size` 預設 `50`、上限 `500`,伺服器端 `Math.Max(page, 1)`、`Math.Clamp(size, 1, 500)`。**MUST NOT** 使用 `pageIndex`、`pageSize`、`limit`、`offset`、`skip`、`take`。 22. **MUST** 分頁回應形狀固定為 `PagedResult<T>`:`{ "total": 1234, "page": 2, "size": 50, "rows": [ ... ] }`,C# 定義為 `record PagedResult<T>(int Total, int Page, int Size, IReadOnlyList<T> Rows)`(與 elf-sqlsugar 同一份)。 回應中的 `page`/`size` **MUST** 是 clamp 後實際使用的值(參考專案回傳原始輸入,是反例)。 23. **新專案 MUST(參考專案未實作)** 排序用 `?sort=<camelCaseField>&order=asc|desc`,未帶 `order` 時為 `desc`。endpoint **MUST** 以白名單 `switch` 把字串轉成 enum,未知值回 `400`(`query.invalid_sort` / `query.invalid_order`);service 只收 enum。**MUST NOT** 把字串直接拼進 SQL 或 `OrderBy(string)`。 24. **MUST** 篩選用具名 query 參數(`?category=basic&level=A1`、`?topic=food&q=apple`):關鍵字搜尋固定叫 `q`;日期範圍用 `from`/`to`(ISO 8601);天數用 `days`;多值用重複參數 `?tag=a&tag=b`(綁定 `string[] tag`)。**MUST NOT** 自訂逗號分隔格式。 25. **MUST** 語系用 `?locale=zh-TW`;未帶時讀 `Accept-Language`,並**依 `q` 值排序**(不是依出現順序),`q=0` 代表不接受。 範本:[`references/server-templates.md` §2](references/server-templates.md#2-分頁排序篩選)。 ### 2.5 版本控制 26. **新專案 MUST(參考專案未實作)** 第一版不加版本段:`/api/<resources>`。出現**無法向下相容**的變更時,只為受影響的 endpoint 新增 `app.MapGroup("/api/v2")`,舊路由保留到前端全部切換後再移除,並在 `docs/api-contract.md` 標註 `deprecated`。 27. **MUST** 以下屬於相容變更、**不需要**開新版本:新增 endpoint、回應新增欄位、新增選填 query 參數。以下屬於破壞性變更、**需要**新版本或同 PR 同步改前端:刪除/改名欄位、改型別、改狀態碼語意、選填變必填。 28. **MUST NOT** 用 header(`api-version`)或 query(`?v=2`)分版——nginx 只按路徑轉發,前端 base URL 也只認路徑。 ### 2.6 認證 29. **MUST** 認證 header 固定為 `Authorization: Bearer <jwt>`。**MUST NOT** 自訂 `X-Token`、`X-Auth`,**MUST NOT** 把 token 放 query string。 30. **MUST** 登入 `POST /auth/login`、註冊 `POST /auth/register`,request 為 `LoginRequest { username, password }`,回傳 `AuthUserDto`(含 `token`)。 31. **新專案 MUST(參考專案未實作)** 以 `AddAuthentication().AddJwtBearer()` + `AddAuthorization()` 驗證;受保護的 group 呼叫 `.RequireAuthorization()`;`/auth/*` 與 `/health` 呼叫 `.AllowAnonymous()`。JWT 金鑰走 `appsettings` / 環境變數(`Jwt__Key`),**MUST NOT** 寫死在程式碼。Claim 名稱一律用 `ElfClaimTypes`(`sub`、`company_id`、`role`)。 ### 2.7 CORS 32. **MUST** 使用具名 policy,origin 從設定 `Cors:Origins`(字串陣列)讀取,程式內 fallback 只放本機開發 origin(範本見 §3.1)。 33. **MUST NOT** 使用 `AllowAnyOrigin()`,更 **MUST NOT** 搭配 `AllowCredentials()`。 34. **MUST** 正式環境走 nginx 同源代理(瀏覽器只打 `/api`,無跨來源請求),因此 `Cors:Origins` 只列開發伺服器與桌面殼,例如 `http://localhost:5173`、`http://tauri.localhost`、`https://tauri.localhost`。 ### 2.8 Forwarded headers 與 rate limiting 35. **MUST** 啟用 `UseForwardedHeaders()`,並放在管線**最前面**(在 `UseExceptionHandler`、`UseCors` 之前);API 永遠在 nginx 後面,否則 client IP 都是 nginx 容器。**新專案 MUST(參考專案未實作)** 使用內建 `AddRateLimiter()`,至少對 `/auth/*` 套用依 client IP 分區的 fixed-window policy,被拒回 `429` + `rate_limit.exceeded`。**MUST NOT** 呼叫 `UseHttpsRedirection()`(TLS 在 nginx 結束)。範本:[`references/server-templates.md` §5](references/server-templates.md#5-rate-limiting-與-forwarded-headers)。 ### 2.9 OpenAPI 36. **MUST** 使用內建 `Microsoft.AspNetCore.OpenApi`:`builder.Services.AddOpenApi();`,並**只在 Development** 執行 `app.MapOpenApi();`(文件位於 `/openapi/v1.json`)。**MUST NOT** 另外安裝 Swashbuckle。 37. **MUST** handler 回傳 `TypedResults`(例 `Task<Results<Ok<CategoryDto>, ProblemHttpResult>>`),或補 `.Produces<T>(200)` / `.ProducesProblem(404)`,讓 OpenAPI 推得出回應型別。 ### 2.10 健康檢查 38. **MUST** 提供匿名的 `GET /api/health`(路徑已定案),回 `200` + `{ "status": "ok", "utc": "<ISO 8601>" }`,並 `.WithName("Health")`。容器 healthcheck **MUST NOT** 依賴 `wget`(`aspnet` 映像沒有);由 **elf-cicd-docker** 的 `api.Dockerfile` 在最終階段以 `apt-get install -y --no-install-recommends curl` 安裝 curl,compose 以 `curl -fsS http://localhost:8080/api/health` 探測。 39. **新專案 MUST(參考專案未實作)** 需要檢查 PostgreSQL 連線時,另開 `GET /api/health/ready`;`/api/health` 保持不碰 DB,避免 DB 短暫抖動讓容器被判定不健康。 ### 2.11 JSON 慣例 40. **MUST** 維持 ASP.NET Core 預設 web JSON 設定:屬性名 **camelCase**、`null` 照常輸出。**MUST NOT** 用 `[JsonPropertyName]` 改成其他命名風格。 41. **MUST** 時間一律 UTC、ISO 8601 字串(`2026-09-27T08:00:00Z`);enum 以 **camelCase 字串**輸出(`new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)`,`SpeedUnit.Wpm` → `"wpm"`),值與前端 `types.ts` 字串聯集完全一致。 42. **MUST** DTO 用 positional `record`:`public record CategoryDto(int Id, string Code, string Name, string Color);`。Request DTO 命名 `<Name>Request`,Response DTO 命名 `<Name>Dto`;名稱與 `types.ts` 的 interface 完全相同(elf-api-contract 規則 8)。 --- ## 3. 標準範本 ### 3.1 `Program.cs`(管線順序即規範) ```csharp using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.HttpOverrides;
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看