Back to skills
SKILL.md
Server Api
ASecurityUse for Nuxt or Nitro server API routes, defineEventHandler, request validation, or server-side auth data access.
- 44 stars
- 0 votes
- 0 copies
- 4 views
- Added February 7, 2026
Works with
Security analysis
100/100npx -y skills add Charles5277/nuxt-supabase-starter --skill server-api --agent claude-codeAre you the author of Server Api?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/charles5277-server-api)---
name: server-api
description: "Use for Nuxt or Nitro server API routes, defineEventHandler, request validation, or server-side auth data access."
---
<!-- clade-skill-scope: project -->
# Server API 設計規範
依實際安裝的 Nuxt/DB/auth 模組選用。Supabase helper(`requireRole`、`getSupabaseWithContext` 等)是 consumer 自己實作的函式,使用前查其來源與授權行為。
## Client 端寫入預設走 Server API
Client 端(`app/` 目錄)的**讀取**路徑依 consumer manifest(canonical `.clade/manifest.json`;canonical 缺席的未遷移 consumer 讀 legacy `.claude/hub.json`)的 `modules.auth` 而定:
- `modules.auth` 是 **Supabase Auth**(`supabase-self-hosted`)→ 預設只透過 `useSupabaseClient<Database>()` 執行 `.select()` 查詢。
- `modules.auth` 是 **`nuxt-auth-utils` / `better-auth`** → Supabase 不簽 JWT,瀏覽器的 `useSupabaseClient()` **恆以 `anon` role 存取**,登入與否都一樣 —— 要讓 client 讀得到就得把表開成 `TO public`,等於對任何持有 publishable key 的人公開。那些 auth stack 下 client 直讀**不是**預設:**讀寫都走 `/api/v1/*`**,client 端 Supabase 只留 Storage(bucket policy 獨立)。見 [[auth-data-path-consistency]] § Auth 策略與資料路徑的合法組合。
所有寫入操作預設必須透過 Server API(`/api/v1/*`):
```typescript
// ❌ 預設不允許 — client 端直接寫入
const client = useSupabaseClient<Database>();
await client.from("posts").insert({ title: "Hello" });
// ✅ 正確 — 透過 Server API
await $fetch("/api/v1/posts", {
method: "POST",
body: { title: "Hello" },
});
```
Client 端寫入是架構例外,只有在 proposal / ADR 明確記錄並同時滿足下列條件時,才可在 client 使用 `.insert()` / `.update()` / `.delete()` / `.upsert()`:
- RLS policy 能完整表達 tenant / user scope 與所有授權規則
- GRANT 只開必要 schema / table / operation,不給寬權限
- 不需要 `service_role`、server-only secret、跨表 transaction、workflow transition、audit-chain、storage upload 或外部 API
- 不涉及角色 / 權限、薪資、簽核、出勤、稽核、批次修復等敏感流程
- 有 focused tests 驗證 allow path、deny path、tenant isolation 與 RLS 失敗時的 UI/錯誤處理
## 契約來源
新增 API 時,request/response schema 請定義在 `shared/schemas/`,並由同一個模組導出衍生型別。
- `shared/schemas/*.ts`:Zod schema + 衍生型別
- `shared/types/*.ts`:相容轉發或 UI/view-model 型別,**不是**新的 request/response 真相來源
## 選填欄位的 Null 慣例(Hard Rule)
**接收 user input 的選填欄位 MUST 用 `.nullish()`,NEVER 只用 `.optional()`。**
```typescript
// ❌ 錯誤 — 只接受 string | undefined
const schema = z.object({
return_notes: z.string().trim().max(500).optional(),
})
// Client 送 { return_notes: null } → 400 ZodError
```
```typescript
// ✅ 正確 — 接受 string | null | undefined(同時涵蓋 .nullable() + .optional())
const schema = z.object({
return_notes: z.string().trim().max(500).nullish(),
})
```
`.optional()` 不接受 `null`,而 form binding 常送 `value.trim() || null`、DB nullable 欄位的真實值也是 null。
| 欄位來源 | 慣例 |
| ---------------------------------------------- | -------------- |
| 選填的 user input(form / textarea / select) | `.nullish()` |
| Server 內部 default 帶入(不從 client 收) | `.optional()` |
| 必填欄位(required = true) | 不加任何修飾 |
| Discriminator union 內部欄位 | 看 union shape |
新增接 user input 的 schema 後 **MUST** 寫 boundary test 涵蓋 `null` / `undefined` / `''` / 字串值(`testing-anti-patterns.md` Anti-Pattern 6),並 trace 真實 client code 確認送什麼。Review 時 `rg -n "z\.string\(\).*\.optional\(\)" shared/schemas/ server/api/` 逐筆比對 client 是否會送 null。
## 目錄結構
```
server/api/
├── v1/ # 版本化業務 API
│ └── resources/
│ ├── index.get.ts # GET /api/v1/resources(列表)
│ ├── index.post.ts # POST /api/v1/resources(新增)
│ └── [id]/
│ ├── index.get.ts # GET /api/v1/resources/:id
│ ├── index.patch.ts # PATCH /api/v1/resources/:id
│ └── index.delete.ts # DELETE /api/v1/resources/:id
├── auth/ # 認證 API
└── admin/ # 管理員 API
```
### 命名規範
- **檔案名稱**:`index.<method>.ts` 格式
- **路徑參數**:有意義的名稱(`[resourceId]` 優於 `[id]`)
- **API 版本**:`/api/v1/` 前綴
## 權限檢查
```typescript
import { requireRole } from "~~/server/utils/supabase";
const user = await requireRole(event, ["admin", "manager"]);
// 角色階層:admin → manager → staff
```
## 回應格式
| 類型 | 格式 |
| ---- | -------------------------------------------------------------------- |
| 列表 | `{ data: items, pagination: { page, pageSize, total, totalPages } }` |
| 單筆 | `{ data: item }` |
| 新增 | status 201 + `{ data: newItem }` |
| 刪除 | `{ data: { id, deleted_at, hard_deleted } }` |
## 錯誤類型
| 狀態碼 | 使用情境 |
| ------ | --------------------------- |
| 400 | 請求格式錯誤、驗證失敗 |
| 401 | 未認證 |
| 403 | 無權限 |
| 404 | 資源不存在 |
| 409 | 資源衝突(unique key 違反) |
| 500 | 伺服器內部錯誤 |
## 參考資料
| 檔案 | 內容 |
| -------------------------------------------------------- | -------------------------- |
| [references/api-template.md](references/api-template.md) | 完整 API 模板 + Zod Schema |
| [references/pagination.md](references/pagination.md) | 分頁、搜尋、排序、操作日誌 |
## 檢查清單
- [ ] 使用 `index.<method>.ts` 命名
- [ ] 在 `shared/schemas/` 定義 request/response schema 與衍生型別
- [ ] 開頭進行權限檢查(requireRole)
- [ ] 使用 getValidatedQuery / readValidatedBody 驗證輸入
- [ ] 使用 getSupabaseWithContext 取得資料庫連線(`modules.auth` 為 better-auth / nuxt-auth-utils 時等價名為 `getAuthedSupabase`)
- [ ] **授權寫在 handler,不假設 RLS 會過濾**——上述 helper 回的都是 service-role client。先驗角色/操作權限;資料屬於個別使用者或 tenant 時,再以 ownership 或查詢條件限制資料範圍。判準見 [[auth-data-path-consistency]] § Server 側
- [ ] 回傳前使用 response schema `parse()`
- [ ] 回傳統一格式(`{ data, pagination? }`)
- [ ] 新增操作設定 201 狀態碼
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.