how_to_schema.md - maoxiaoyue/hypgo GitHub Wiki
Schema-first 路由:從零開始的完整指南
本頁說明如何在 HypGo 定義 Schema-first 路由。目前專案仍在早期階段,範例以 v0.8.1 為準,若操作時遇到問題,歡迎在 GitHub Issues 回報。
HypGo Framework — How to Write Schema Routes 版本:0.8.1 | 2026-03
什麼是 Schema-first 路由?
在 HypGo 中,你先定義路由的「結構」(Schema),包含各種 HTTP 方法、路徑、輸入輸出型別與簡單描述。HypGo 會根據這些資訊自動產生 manifest.yaml,供 AI 參考,也會用來進行Contract 測試。
這樣做的好處是讓人與AI都對結構更清楚,也方便後續維護與測試,節省Token。
人機分工總覽
在 HypGo 的 Schema-first 開發流程中,人和 AI 各有明確的職責:
步驟 誰負責 產出
──────────────────────────────────────────────────────
1. 定義 Model struct 👤 人寫 app/models/user.go
2. 定義 Schema 路由 👤 人寫 app/routers/user.go
3. 在 router.go 註冊 👤 人寫 app/routers/router.go
4. 實作 Handler 業務邏輯 🤖 AI 生成 app/controllers/user_controller.go
5. Contract Testing 驗證 🔧 框架自動 contract.TestAll() 自動跑
6. 生成 Manifest 🔧 框架自動 .hyp/context.yaml 自動產出
核心原則:
| 人的工作 | AI 的工作 | 框架的工作 | |
|---|---|---|---|
| 定義 | 決定「什麼」(API 長什麼樣) | — | — |
| 實作 | 審核業務邏輯 | 生成「怎麼做」(boilerplate + CRUD) | — |
| 驗證 | — | — | 自動判斷「對不對」(Contract Testing) |
你寫 Schema,AI 寫 Handler,框架驗證結果。
什麼是 Schema-first 路由?
傳統路由只記錄「路徑 → 函式」:
r.POST("/api/users", createUserHandler)
// AI 要理解這個路由,必須讀 createUserHandler 的全部程式碼
Schema-first 路由在註冊時攜帶結構化 metadata:
r.Schema(schema.Route{
Method: "POST",
Path: "/api/users",
Summary: "建立使用者",
Input: CreateUserReq{},
Output: UserResp{},
}).Handle(createUserHandler)
// AI 只需讀這 6 行就理解:POST /api/users,輸入 CreateUserReq,輸出 UserResp
差異:AI 理解一個路由從 ~2,000 tokens 降到 ~200 tokens。
第一步:定義 Model(struct) 👤 人寫
由人負責:決定資料欄位、型別、JSON tag。這是業務決策,AI 無法替你決定「使用者需要哪些欄位」。
Schema 路由引用的 Input/Output 型別定義在 app/models/ 中。
可以手動寫,也可以用 hyp generate model hyp 自動生成骨架(再由人修改欄位):
hyp generate model user
# → app/models/user.go
生成的檔案包含 5 個 struct:
// app/models/user.go
// DB model — 對應資料庫 table
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
Name string `bun:"name,notnull" json:"name"`
Email string `bun:"email,notnull,unique" json:"email"`
Active bool `bun:"active,notnull,default:true" json:"active"`
CreatedAt time.Time `bun:"created_at" json:"created_at"`
}
// Schema Input — POST 請求 body
type CreateUserReq struct {
Name string `json:"name"`
Email string `json:"email"`
}
// Schema Input — PUT 請求 body
type UpdateUserReq struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
}
// Schema Output — 單筆回應
type UserResp struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Schema Output — 列表回應
type UserListResp struct {
Data []UserResp `json:"data"`
Total int `json:"total"`
}
設計原則
- Input struct ≠ DB model — Input 只包含使用者可以提供的欄位(不含 ID、CreatedAt)
- Output struct ≠ DB model — Output 可以省略敏感欄位(如密碼 hash)
- 每個操作一個 Input —
CreateUserReq和UpdateUserReq分開(Update 的欄位通常是 optional)
第二步:定義 Schema 路由 👤 人寫
由人負責:決定 API 路徑、HTTP 方法、Input/Output 型別、可能的錯誤狀態碼。這是 API 設計,是整個人機協作的「起點」和「合約」。
Schema 路由定義在 app/routers/ 中,與 handler(controller)分開。
可以手動寫,也可以用 hyp generate controller 生成骨架(再由人調整路由和型別):
hyp generate controller user
# → app/controllers/user_controller.go (handler)
# → app/routers/user.go (schema 路由)
# → app/routers/router.go (總入口)
# → app/routers/middleware.go (中間件)
完整的 Schema 路由範例
// app/routers/user.go
package routers
import (
"github.com/maoxiaoyue/hypgo/pkg/router"
"github.com/maoxiaoyue/hypgo/pkg/schema"
"myapp/app/controllers"
"myapp/app/models"
)
func RegisterUserRoutes(r *router.Router) {
ctrl := &controllers.UserController{}
// GET /api/user — 列出所有使用者
r.Schema(schema.Route{
Method: "GET",
Path: "/api/user",
Summary: "List all users",
Tags: []string{"user"},
Output: models.UserListResp{},
}).Handle(ctrl.List)
// POST /api/user — 建立使用者
r.Schema(schema.Route{
Method: "POST",
Path: "/api/user",
Summary: "Create user",
Tags: []string{"user"},
Input: models.CreateUserReq{},
Output: models.UserResp{},
Responses: map[int]schema.ResponseSchema{
201: {Description: "User created"},
400: {Description: "Invalid input"},
409: {Description: "Email already exists"},
},
}).Handle(ctrl.Create)
// GET /api/user/:id — 查詢單一使用者
r.Schema(schema.Route{
Method: "GET",
Path: "/api/user/:id",
Summary: "Get user by ID",
Tags: []string{"user"},
Output: models.UserResp{},
Responses: map[int]schema.ResponseSchema{
200: {Description: "User found"},
404: {Description: "User not found"},
},
}).Handle(ctrl.Get)
// PUT /api/user/:id — 更新使用者
r.Schema(schema.Route{
Method: "PUT",
Path: "/api/user/:id",
Summary: "Update user",
Tags: []string{"user"},
Input: models.UpdateUserReq{},
Output: models.UserResp{},
Responses: map[int]schema.ResponseSchema{
200: {Description: "User updated"},
400: {Description: "Invalid input"},
404: {Description: "User not found"},
},
}).Handle(ctrl.Update)
// DELETE /api/user/:id — 刪除使用者
r.Schema(schema.Route{
Method: "DELETE",
Path: "/api/user/:id",
Summary: "Delete user",
Tags: []string{"user"},
Responses: map[int]schema.ResponseSchema{
204: {Description: "User deleted"},
404: {Description: "User not found"},
},
}).Handle(ctrl.Delete)
}
第三步:在 router.go 中註冊 👤 人寫
由人負責:決定哪些路由要啟用、中間件如何組合。這是架構決策。
// app/routers/router.go
package routers
import (
"github.com/maoxiaoyue/hypgo/pkg/router"
"github.com/maoxiaoyue/hypgo/pkg/middleware"
)
func Setup(r *router.Router) {
// 全域中間件
r.Use(middleware.DefaultMiddleware()...)
// 註冊各資源路由
RegisterUserRoutes(r)
RegisterOrderRoutes(r)
RegisterGameRoutes(r)
}
在 main.go 中呼叫
func main() {
srv := server.New(cfg, log)
routers.Setup(srv.Router())
srv.Start()
}
schema.Route 欄位詳解
| 欄位 | 型別 | 必填? | 說明 | 影響 |
|---|---|---|---|---|
Method |
string | ✅ | HTTP 方法(GET/POST/PUT/DELETE) | 路由註冊 |
Path |
string | ✅ | 路由路徑(支援 :id、*filepath) |
路由註冊 |
Summary |
string | 強烈建議 | 一句話描述,AI 靠它理解意圖 | Manifest 顯示 |
Input |
interface{} | POST/PUT 建議 | 請求 body 的 Go struct | Contract 驗證 Input |
Output |
interface{} | 建議 | 回應 body 的 Go struct | Contract 驗證 Output |
Tags |
[]string | 建議 | 分類標籤(如 "users"、"admin") | Manifest 分組 |
Description |
string | 可選 | 更詳細的說明 | Manifest 顯示 |
Responses |
map[int]ResponseSchema | 可選 | 各狀態碼的描述 | Contract 狀態碼推測 |
Params |
[]ParamSchema | 可選 | 路徑/查詢/標頭參數描述 | 文檔用途 |
Headers |
[]HeaderSchema | 可選 | 必要的請求標頭 | 文檔用途 |
哪些欄位影響什麼功能
Schema 欄位 → Manifest → Contract → AI 理解
─────────────────────────────────────────────────────────
Method + Path → 路由列表 → 請求方法 → API 端點
Summary → 路由描述 → — → 路由意圖
Input → input_type → 驗證請求 body → 輸入型別
Output → output_type → 驗證回應 body → 輸出型別
Tags → 分組 → — → 模組劃分
Responses → 狀態碼 → 推測期望狀態碼 → 錯誤情境
第三.五步:AI 生成 Handler 🤖 AI 生成
由 AI 負責:根據你定義的 Schema(Input/Output 型別、錯誤碼),生成 handler 的實作程式碼。你只需審核業務邏輯是否正確。
完成步驟一到三後,告訴 AI:
讀取 .hyp/context.yaml 了解專案結構。
根據 app/routers/user.go 中的 Schema 定義,
實作 app/controllers/user_controller.go 中所有 handler 的業務邏輯。
使用 Bun ORM 存取資料庫,錯誤碼使用 errors.Define()。
AI 會根據 Schema 中的 Input(CreateUserReq)和 Output(UserResp)生成:
// 🤖 AI 生成(人審核)
func (ctrl *UserController) Create(c *hypcontext.Context) {
var req models.CreateUserReq // ← AI 知道 Input 是 CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
errors.AbortWithAppError(c, ErrUserInvalid.With("reason", err.Error()))
return
}
user := &models.User{Name: req.Name, Email: req.Email} // ← AI 知道欄位
_, err := db.WriteHypDB().NewInsert().Model(user).Exec(ctx)
if err != nil {
errors.AbortWithAppError(c, ErrUserInvalid.With("reason", "db error"))
return
}
c.JSON(201, models.UserResp{ // ← AI 知道 Output 是 UserResp
ID: user.ID,
Name: user.Name,
Email: user.Email,
})
}
人需要審核:
- 業務邏輯是否正確(例如:建立使用者前是否要檢查 email 唯一性?)
- 資料庫操作是否合理(是否需要 transaction?)
- 錯誤處理是否完整
人不需要操心:
- Input 解析(AI 從 Schema 知道用
CreateUserReq) - Output 格式(AI 從 Schema 知道回
UserResp) - 錯誤碼格式(AI 使用
errors.Define+AbortWithAppError)
第四步:Contract Testing 自動驗證 🔧 框架自動
由框架負責:根據你在第二步定義的 Schema,自動生成測試資料、發送請求、驗證回應。你只需寫一行
contract.TestAll()。
Schema 寫好後,Contract Testing 自動驗證 handler 是否符合合約:
// app/routers/router_test.go
package routers
import (
"testing"
"github.com/maoxiaoyue/hypgo/pkg/contract"
"github.com/maoxiaoyue/hypgo/pkg/router"
)
func setupRouter() *router.Router {
r := router.New()
Setup(r)
return r
}
// 一行測試所有 schema 路由
func TestAllContracts(t *testing.T) {
contract.TestAll(t, setupRouter())
}
// 手動測試特定路由
func TestCreateUser(t *testing.T) {
contract.Test(t, setupRouter(), contract.TestCase{
Route: "POST /api/user",
Input: `{"name":"alice","email":"[email protected]"}`,
ExpectStatus: 201,
ExpectSchema: true,
})
}
TestAll 內部做了什麼
1. 遍歷 Schema Registry 所有路由
2. 為每個路由的 Input struct 自動生成 JSON(字串 → "test",數字 → 0)
3. 發送 HTTP 請求到 handler
4. 驗證狀態碼(POST → 201,DELETE → 204,其他 → 200)
5. 驗證回應 body 的 JSON 欄位符合 Output struct
6. 驗證 Output struct 的必填欄位(non-pointer、non-omitempty)全部存在
第五步:生成 Manifest 🔧 框架自動
由框架負責:自動掃描 Schema Registry,產出包含所有路由、型別、設定的 YAML/JSON 檔案。AI 讀這個檔案就能理解整個專案。
Schema 定義完成後,框架自動將 metadata 注入 Manifest:
hyp context -o .hyp/context.yaml
產出:
routes:
- method: POST
path: /api/user
summary: "Create user"
tags: [user]
input_type: CreateUserReq
output_type: UserResp
handler_names: [controllers.(*UserController).Create]
responses:
201: "User created"
400: "Invalid input"
AI 讀這個檔案就知道所有路由的完整資訊。
使用 Group 的 Schema 路由
Group 的 Schema() 方法會自動加上 basePath 前綴:
api := r.NewGroup("/api/v1")
api.Schema(schema.Route{
Method: "GET",
Path: "/products", // 實際註冊為 /api/v1/products
Summary: "List products",
Output: ProductListResp{},
}).Handle(listProducts)
傳統路由 vs Schema 路由:什麼時候用哪個
| 場景 | 建議 | 原因 |
|---|---|---|
| CRUD API(對外) | Schema 路由 | AI 可理解、Contract 可驗證 |
| 會請 AI 幫忙實作的路由 | Schema 路由 | AI 從 metadata 生成更準確的程式碼 |
內部健康檢查 /health |
傳統路由 | 太簡單不需要 schema |
WebSocket 升級 /ws |
傳統路由 | WebSocket 不是 REST |
靜態檔案 /static/* |
傳統路由 | r.Static() 已夠用 |
| 第三方 Webhook 接收 | 傳統路由 | Input 格式由第三方決定 |
兩種路由可以並存:
// Schema 路由
r.Schema(schema.Route{...}).Handle(handler)
// 傳統路由
r.GET("/health", healthHandler)
r.Static("/static", "./public")
常見錯誤
1. Input/Output 填了 nil
// ❌ 不好
r.Schema(schema.Route{
Method: "POST", Path: "/api/users",
Summary: "Create user",
// Input: nil ← Contract 無法驗證請求
// Output: nil ← Contract 無法驗證回應
}).Handle(handler)
// ✅ 正確
r.Schema(schema.Route{
Method: "POST", Path: "/api/users",
Summary: "Create user",
Input: CreateUserReq{},
Output: UserResp{},
}).Handle(handler)
2. Summary 留空
// ❌ AI 看 manifest 不知道這路由幹嘛
r.Schema(schema.Route{
Method: "POST", Path: "/api/orders",
}).Handle(handler)
// ✅ 一句話說明意圖
r.Schema(schema.Route{
Method: "POST", Path: "/api/orders",
Summary: "Create order",
}).Handle(handler)
3. Input struct 用了 DB model
// ❌ DB model 含有 ID、CreatedAt,使用者不該提供這些
r.Schema(schema.Route{
Input: models.User{}, // 包含 ID, CreatedAt 等
}).Handle(handler)
// ✅ 用專屬的 Request struct
r.Schema(schema.Route{
Input: models.CreateUserReq{}, // 只有 Name, Email
}).Handle(handler)
4. Responses 不寫
// ❌ AI 不知道可能有哪些錯誤
r.Schema(schema.Route{
Method: "POST", Path: "/api/users",
}).Handle(handler)
// ✅ 列出所有可能的狀態碼
r.Schema(schema.Route{
Method: "POST", Path: "/api/users",
Responses: map[int]schema.ResponseSchema{
201: {Description: "Created"},
400: {Description: "Validation error"},
409: {Description: "Email conflict"},
},
}).Handle(handler)
完整流程:誰在什麼時候做什麼
👤 人 🤖 AI 🔧 框架
│ │ │
1. ├─ 定義 Model struct ────→│ │
│ (欄位、型別、JSON tag) │ │
│ │ │
2. ├─ 定義 Schema 路由 ──────→│ │
│ (Input/Output/Responses)│ │
│ │ │
3. ├─ 在 router.go 註冊 ────→│ │
│ (Setup + 中間件) │ │
│ │ │
3.5 ├─ 描述需求 ─────────────→├─ 生成 Handler ──────────→│
│ "實作 Create handler" │ (解析 Input、回傳 Output) │
│ │ │
4. │ │ ├─ Contract 驗證
│ │ │ (自動測試 Input/Output)
│ │ │
│ │ ← 失敗回饋 ──────────────┤
│ ├─ 修正 Handler ──────────→├─ 再次驗證 ✅
│ │ │
5. │ │ ├─ 生成 Manifest
│ │ │ (.hyp/context.yaml)
│ │ │
├─ 審核業務邏輯 ←──────────┤ │
│ (檢查 AI 的實作) │ │
│ │ │
└─ 完成 ✅ │ │
為什麼這個分工有效
| 角色 | 擅長 | 不擅長 |
|---|---|---|
| 👤 人 | 業務需求、API 設計、安全性判斷 | 重複的 boilerplate、記住所有欄位名 |
| 🤖 AI | 根據明確 spec 生成程式碼、遵循慣例 | 業務需求判斷、安全性決策 |
| 🔧 框架 | 自動驗證介面合約、生成文檔 | 理解業務邏輯 |
Schema 是人和 AI 之間的「合約」— 人定義合約,AI 按合約實作,框架驗證合約是否被遵守。
快速參考
# 生成 model + controller + router(推薦順序)
hyp generate model user
hyp generate controller user
# 生成 manifest
hyp context -o .hyp/context.yaml
# 驗證所有 schema 路由
go test ./app/routers/... -v
# 分析修改影響
hyp impact app/routers/user.go
HypGo · Schema-first Routes Guide · 2026-03