跳转到内容

构建 REST 资源(x/rest)

本指南展示如何使用 x/rest 构建标准 CRUD API。通过类型化的仓储层,用最少的样板代码即可获得分页列表、详情、创建、更新、删除和局部更新端点。

边界说明见 x/rest Primer

x/rest 在当前发布矩阵中仍属于 experimental 扩展家族。生产路径中建议通过应用本地 controller 或 adapter 隔离它,并在假设 API 或配置兼容性已冻结前先查看发布策略扩展成熟度

目标API 表面结果
定义资源模型应用自己的类型create/list/show 响应的 JSON 形态
构建仓储rest.NewSQLBuilderrest.NewBaseRepository[T]FindAllFindByIDCreateUpdateDeleteCountExists
接入 CRUD 路由rest.NewDBResourceController[T]rest.RegisterResourceRoutes六个标准 resource endpoint
解析列表查询rest.NewQueryBuilder().WithPageSize(...).Parse(r)分页、排序、过滤、搜索和字段限制
自定义行为嵌入 rest.BaseResourceController只覆盖需要业务逻辑的 handler
User type
-> SQLBuilder
-> BaseRepository[User]
-> DBResourceController[User]
-> RegisterResourceRoutes(...)
-> contract.WriteResponse / contract.WriteError
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}

对 SQL 支持的资源使用 rest.NewBaseRepository,通过 SQLBuilder 配置表名、ID 列和列列表:

import (
"github.com/spcent/plumego/x/rest"
"github.com/spcent/plumego/store/db"
)
builder := rest.NewSQLBuilder("users", "id").
WithColumns("id", "name", "email", "created_at").
WithScanFunc(func(rows *sql.Rows) (any, error) {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
return u, err
}).
WithInsertFunc(func(v any) ([]any, error) {
u := v.(User)
return []any{u.ID, u.Name, u.Email, u.CreatedAt}, nil
}).
WithUpdateFunc(func(v any) ([]any, error) {
u := v.(User)
return []any{u.Name, u.Email}, nil
})
repo := rest.NewBaseRepository[User](sqlDB, builder)

BaseRepository[T] 实现了 rest.Repository[T],处理带分页和过滤的 FindAllFindByIDCreateUpdateDeleteCountExists

controller := rest.NewDBResourceController[User]("users", repo)

一次性注册所有 CRUD 路由:

import (
"github.com/spcent/plumego/router"
"github.com/spcent/plumego/x/rest"
)
r := router.NewRouter()
rest.RegisterResourceRoutes(r, "/api/v1/users", controller, rest.RouteOptions{})

注册的路由:

方法路径Handler
GET/api/v1/userscontroller.Index
GET/api/v1/users/:idcontroller.Show
POST/api/v1/userscontroller.Create
PUT/api/v1/users/:idcontroller.Update
DELETE/api/v1/users/:idcontroller.Delete
PATCH/api/v1/users/:idcontroller.Patch

列表请求与响应 envelope 示例:

GET /api/v1/users?page=1&page_size=25&sort=name&email=alice@example.com
{
"data": [
{
"id": "user_123",
"name": "Alice",
"email": "alice@example.com"
}
],
"meta": {
"total": 1,
"page": 1,
"size": 25
}
}

客户端可以在列表端点使用以下查询参数:

GET /api/v1/users?page=2&page_size=25&sort=name&email=alice@example.com&search=alice
参数类型说明
pageint页码(从 1 开始)
page_sizeint每页条数
sortstring列名;加 - 前缀降序(如 -created_at
任意非保留查询键,如 emailstring按列精确过滤
searchstring全文搜索字符串(实现相关)
fields逗号分隔限制返回字段

Builder 配置:

Builder 调用效果
WithPageSize(25, 100)默认每页 25,最大每页 100
WithAllowedSorts("name", "created_at")拒绝未允许的排序字段
WithAllowedFilters("email")允许 email 作为精确过滤条件
Parse(r)读取当前 *http.Request 并返回 rest.QueryParams

在自定义 handler 中使用 rest.QueryBuilder 解析:

import "github.com/spcent/plumego/x/rest"
func (h UserHandler) List(w http.ResponseWriter, r *http.Request) {
params := rest.NewQueryBuilder().
WithPageSize(25, 100).
WithAllowedSorts("name", "created_at").
WithAllowedFilters("email").
Parse(r)
users, total, err := h.repo.FindAll(r.Context(), params)
if err != nil { ... }
contract.WriteResponse(w, r, http.StatusOK, users, map[string]any{
"total": total,
"page": params.Page,
"size": params.PageSize,
})
}

当生成的控制器不合适时(如需要业务验证或自定义响应格式),嵌入 rest.BaseResourceController 并覆盖需要的方法:

type UserController struct {
rest.BaseResourceController
svc *UserService
}
func NewUserController(svc *UserService) *UserController {
return &UserController{
BaseResourceController: *rest.NewBaseResourceController("users"),
svc: svc,
}
}
func (c *UserController) Create(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeValidation).
Code(contract.CodeInvalidJSON).
Message("invalid request body").
Build())
return
}
user, err := c.svc.Create(r.Context(), req)
if err != nil { ... }
contract.WriteResponse(w, r, http.StatusCreated, user, nil)
}
// Index、Show、Update、Delete、Patch 走向 BaseResourceController
// 并返回 501,除非你覆盖它们。
检查项建议
endpoint 是否位于生产关键路径?用应用本地 controller 或 adapter 隔离 x/rest
是否需要自定义验证、认证或响应形态?显式覆盖 handler,不要拉伸生成行为
是否依赖查询语义?因为 x/rest 仍是 experimental,应在应用测试中固定行为
是否要为消费者写 API 文档?记录应用 endpoint contract,不要记录 experimental 包内部细节
现象先检查
查询解析代码无法编译使用 rest.NewQueryBuilder().WithPageSize(...).Parse(r),不要使用绑定 request 的 builder 构造函数
路由没有进入 handler确认 rest.RegisterResourceRoutes 在应用本地路由 wiring 中调用,并且使用的 router 就是最终被服务的 router
仓储调用无法编译确认你的仓储实现了当前 controller 使用的 rest.Repository[T] 方法
列表响应形态不对断言 contract.WriteResponse envelope:data 放列表,meta 放分页信息
生产升级风险不清楚检查 x/rest/module.yaml发布策略,并用应用测试固定查询语义