构建 REST 资源(x/rest)
构建 REST 资源
Section titled “构建 REST 资源”本指南展示如何使用 x/rest 构建标准 CRUD API。通过类型化的仓储层,用最少的样板代码即可获得分页列表、详情、创建、更新、删除和局部更新端点。
边界说明见 x/rest Primer。
发布姿态说明
Section titled “发布姿态说明”x/rest 在当前发布矩阵中仍属于 experimental 扩展家族。生产路径中建议通过应用本地 controller 或 adapter 隔离它,并在假设 API 或配置兼容性已冻结前先查看发布策略和扩展成熟度。
| 目标 | API 表面 | 结果 |
|---|---|---|
| 定义资源模型 | 应用自己的类型 | create/list/show 响应的 JSON 形态 |
| 构建仓储 | rest.NewSQLBuilder、rest.NewBaseRepository[T] | FindAll、FindByID、Create、Update、Delete、Count、Exists |
| 接入 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第一步 — 定义资源类型
Section titled “第一步 — 定义资源类型”type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"created_at"`}第二步 — 构建仓储
Section titled “第二步 — 构建仓储”对 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],处理带分页和过滤的 FindAll、FindByID、Create、Update、Delete、Count 和 Exists。
第三步 — 接入控制器
Section titled “第三步 — 接入控制器”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/users | controller.Index |
| GET | /api/v1/users/:id | controller.Show |
| POST | /api/v1/users | controller.Create |
| PUT | /api/v1/users/:id | controller.Update |
| DELETE | /api/v1/users/:id | controller.Delete |
| PATCH | /api/v1/users/:id | controller.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 }}第四步 — 查询参数
Section titled “第四步 — 查询参数”客户端可以在列表端点使用以下查询参数:
GET /api/v1/users?page=2&page_size=25&sort=name&email=alice@example.com&search=alice| 参数 | 类型 | 说明 |
|---|---|---|
page | int | 页码(从 1 开始) |
page_size | int | 每页条数 |
sort | string | 列名;加 - 前缀降序(如 -created_at) |
任意非保留查询键,如 email | string | 按列精确过滤 |
search | string | 全文搜索字符串(实现相关) |
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, })}第五步 — 自定义控制器
Section titled “第五步 — 自定义控制器”当生成的控制器不合适时(如需要业务验证或自定义响应格式),嵌入 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,除非你覆盖它们。生产采用检查
Section titled “生产采用检查”| 检查项 | 建议 |
|---|---|
| endpoint 是否位于生产关键路径? | 用应用本地 controller 或 adapter 隔离 x/rest |
| 是否需要自定义验证、认证或响应形态? | 显式覆盖 handler,不要拉伸生成行为 |
| 是否依赖查询语义? | 因为 x/rest 仍是 experimental,应在应用测试中固定行为 |
| 是否要为消费者写 API 文档? | 记录应用 endpoint contract,不要记录 experimental 包内部细节 |
如果没有按预期工作
Section titled “如果没有按预期工作”| 现象 | 先检查 |
|---|---|
| 查询解析代码无法编译 | 使用 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、发布策略,并用应用测试固定查询语义 |