Skip to content

APIs

Typed handlers

api.Register binds and validates the input struct, writes the output as JSON and describes the route for OpenAPI.

Last updated:

Go
type StoreProduct struct {
	ShopID int64   `json:"shop"  validate:"required,positive"`
	Name   string  `json:"name"  validate:"required,max=80" doc:"Display name"`
	Price  float64 `json:"price" validate:"min=0"`
}

r.Group("/api/v1/shops/{shop}", func(g *router.Group) {
	g.Use(auth.Required(apiGuard)) // Op.Security only documents the scheme

	api.Register(g, "POST /products", api.Op{
		Name:     "products.store",
		Summary:  "Create a product",
		Tags:     []string{"products"},
		Security: []string{"bearer"},
	}, func(c *web.Ctx, in StoreProduct) (resources.Product, error) {
		p := models.Product{ShopID: in.ShopID, Name: in.Name, Price: in.Price}
		if err := orm.Create(c.Context(), db, &p); err != nil {
			return resources.Product{}, err
		}

		return resources.FromProduct(p), nil
	})
})

Input#

  • The input is built from the query string, the JSON or form body and the path parameters, in that order; path parameters win over body fields of the same name (a body cannot change {shop}).
  • GET, HEAD and DELETE read only the query string.
  • It is validated with the input struct's validate tags (the same rules as web.Bind); failures answer 422 with field errors, malformed JSON answers 400.
  • Use struct{} as the input type when there is none.

Output#

CaseStatus
Op.Status setthat status
output type is struct{}204, no body
POST201
otherwise200

Errors returned by the handler go through the normal problem+json path.

Response validation#

Set Op.ValidateResponse: true to check the output against its validate tags before it is written (the output type must be a struct, otherwise Register panics at boot):

Go
api.Register(g, "GET /tasks", api.Op{Name: "tasks.index", ValidateResponse: true}, tasks.Index)

The check runs when web.Services.CheckResponses is on (the same switch as web.JSONWith). The app turns it on whenever APP_ENV is not production, that is development or test: a staging deployment that runs with APP_ENV=development checks too and answers 500 on a mismatch. A mismatch is a server bug: it is logged and answered with a generic 500 instead of sending data that breaks the documented contract. In production the output is written without the check.

A list field tagged validate:"required" must be present and not null (a nil slice fails), but may be empty; use min=1 when it needs items.

Route metadata#

Op.Name names the route (and becomes the OpenAPI operationId). Every typed route carries an *api.Operation with its method, full path (group prefixes included), path parameters and input/output types, readable from router.RouteInfo.Doc. That is what the OpenAPI generator uses; plain web.HandlerFunc routes keep working beside typed ones.

Op.Security only declares the security scheme in the OpenAPI document; it does not authenticate anything. Protect the route or its group with auth.Required (and auth.Ability for token abilities).