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)
  • 每個操作一個 InputCreateUserReqUpdateUserReq 分開(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 中的 InputCreateUserReq)和 OutputUserResp)生成:

// 🤖 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