以下创建一个 REST API 应用的最佳实践
库包:
模型
使用ORM模型,在本例中,gorm使用该模型将结构转换为 SQL 语句。例如:
type Workspace struct { ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()" json:"id"` Name string `gorm:"not null,type:text" json:"name"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index,->" json:"-"` }
- ID用作主键,使用随机 UUID 而不是自增整数。
- name是模型的一个属性,它可以是任何名称下的任何有限数量的东西。
- 创建模型时的CreatedAt由 gorm 自动处理。
- UpdatedAt模型更新时,由 gorm 自动处理。
- DeletedAt这是 gorm 处理软删除的方式。它需要是 gorm.DeletedAt 的类型
存储库
存储库是一种设计模式,可以帮助我们进行CRUD操作。
我们先定义一个接口:
type Repository interface { Configure(*gorm.DB) List(after time.Time, limit int) (any, error) Get(id any) (any, error) Create(entity any) (any, error) Update(id any, entity any) (bool, error) Delete(id any) (bool, error) }
然后他们使用 gorm 作为 ORM 的实现:
func (repository *WorkspaceRepository) List(after time.Time, limit int) (any, error) { var wc model.WorkspaceCollection order := "created_at" err := r.db.Limit(limit).Order(order).Where(fmt.Sprintf("%v > ?", order), after).Limit(limit).Find(&wc).Error return wc, err } func (repository *WorkspaceRepository) Get(id any) (any, error) { var w *model.Workspace err := r.db.Where("id = ?", id).First(&w).Error return w, err } func (repository *WorkspaceRepository) Create(entity any) (any, error) { w := entity.(*model.Workspace) err := r.db.Create(w).Error return w, err } func (repository *WorkspaceRepository) Update(id any, entity any) (bool, error) { w := entity.(*model.Workspace) if err := r.db.Model(w).Where("id = ?", id).Updates(w).Error; err != nil { return false, err } return true, nil } func (repository *WorkspaceRepository) Delete(id any) (bool, error) { if err := r.db.Delete(&model.Workspace{}, "id = ?", id).Error; err != nil { return false, err } return true, nil }
路由
为了处理 HTTP 路由,我们需要创建一些称为控制器的函数,在这个例子中,使用了Gin。
func (server *Server) registerRoutes() { var router = server.router workspaces := router.Group("/workspaces") { workspaces.GET("", GetWorkspaces) workspaces.POST("", CreateWorkspace) workspaces.GET("/:uuid", GetWorkspace) workspaces.PATCH("/:uuid", UpdateWorkspace) workspaces.DELETE("/:uuid", DeleteWorkspace) } }
控制器
控制器负责处理 HTTP 调用并返回有用的信息,这些信息可以是带有来自 ORM 的对象的 JSON 或错误。让我们实现所有的 CRUD 操作:
func GetWorkspaceRepository(ctx *gin.Context) repository.Repository { return ctx.MustGet("RepositoryRegistry").(*repository.RepositoryRegistry).MustRepository("WorkspaceRepository") } func GetWorkspaces(ctx *gin.Context) { var q = query{} if err := ctx.ShouldBindQuery(&q); err != nil { HandleError(err, ctx) return } entities, err := GetWorkspaceRepository(ctx).List(q.After, q.Limit) if err != nil { HandleError(err, ctx) return } WriteHAL(ctx, http.StatusOK, entities.(model.WorkspaceCollection).ToHAL(ctx.Request.URL.Path, ctx.Request.URL.Query())) } func GetWorkspace(ctx *gin.Context) { p := params{} ctx.ShouldBindUri(&p) if err := validate.Struct(p); err != nil { HandleError(err, ctx) return } entity, err := GetWorkspaceRepository(ctx).Get(p.ID) if err != nil { HandleError(err, ctx) return } WriteHAL(ctx, http.StatusOK, entity.(*model.Workspace).ToHAL(ctx.Request.URL.Path)) } func CreateWorkspace(ctx *gin.Context) { body := model.Workspace{} if err := ctx.BindJSON(&body); err != nil { HandleError(err, ctx) return } entity, err := GetWorkspaceRepository(ctx).Create(&body) if err != nil { HandleError(err, ctx) return } workspace := entity.(*model.Workspace) selfHref, _ := url.JoinPath(ctx.Request.URL.Path, workspace.ID.String()) WriteHAL(ctx, http.StatusCreated, workspace.ToHAL(selfHref)) } func UpdateWorkspace(ctx *gin.Context) { p := params{} ctx.ShouldBindUri(&p) if err := validate.Struct(p); err != nil { HandleError(err, ctx) return } body := model.Workspace{} if err := ctx.BindJSON(&body); err != nil { HandleError(err, ctx) return } repository := GetWorkspaceRepository(ctx) _, err := repository.Update(p.ID, &body) if err != nil { HandleError(err, ctx) return } entity, err := repository.Get(p.ID) if err != nil { HandleError(err, ctx) return } WriteHAL(ctx, http.StatusOK, entity.(*model.Workspace).ToHAL(ctx.Request.URL.Path)) } func DeleteWorkspace(ctx *gin.Context) { p := params{} ctx.ShouldBindUri(&p) if err := validate.Struct(p); err != nil { HandleError(err, ctx) return } _, err := GetWorkspaceRepository(ctx).Delete(p.ID) if err != nil { HandleError(err, ctx) return } WriteNoContent(ctx) }
HAL 链接
API 是永恒的。一旦将 API 集成到生产应用程序中,就很难进行可能破坏现有集成的重大更改
Web API 设计原则:使用 API 和微服务交付价值
在实践中,很难打破 API 契约,因为 API 使用者会生你的气。新版本的 API 不实用;没有人会转移到另一个 API。
考虑到这一点,正式名称为 JSON 超文本应用程序语言的HAL Links尝试以一种没有痛苦的方式解决 API 迁移。API 应该在self字段中返回资源的表示,而不是对资源使用硬编码的位置。
{ "_links": { "self": "/workspaces/6424f2b7-8094-48de-a68c-24bbb7de1faa" } } ...
实现非常简单:
func (model *Workspace) ToHAL(selfHref string) (root hal.Resource) { root = hal.NewResourceObject() root.AddData(model) selfRel := hal.NewSelfLinkRelation() selfLink := &hal.LinkObject{Href: selfHref} selfRel.SetLink(selfLink) root.AddLink(selfRel) return }
问题
你可能已经注意到每个错误都会调用HandleError函数,这个函数负责通过返回application/problem+json将错误变成更有意义的东西。
func HandleError(err error, ctx *gin.Context) { var p *problem.Problem switch { case errors.Is(err, gorm.ErrRecordNotFound): p = problem.New( problem.Title("Record Not Found"), problem.Type("errors:database/record-not-found"), problem.Detail(err.Error()), problem.Status(http.StatusNotFound), ) break default: p = problem.New( problem.Title("Bad Request"), problem.Type("errors:http/bad-request"), problem.Detail(err.Error()), problem.Status(http.StatusBadRequest), ) break } p.WriteTo(ctx.Writer) }
例如,如果after参数不是RFC 3339的格式。它将返回一个错误
$ http localhost:8000/workspaces?after=0 HTTP/1.1 400 Bad Request Content-Length: 164 Content-Type: application/problem+json Date: Sat, 30 Jul 2022 18:23:54 GMT { "detail": "parsing time \Ŕ\" as \-01-02T15:04:05Z07:00\": cannot parse \Ŕ\" as \\"", "status": 400, "title": "Bad Request", "type": "errors:http/bad-request" }
注意Content-Type,它是HTTP APIs的问题细节的mimetype,正文中有一个详细的错误。