Preparing your codebase
Gohandlers enforces the best development habits on handler definitions in terms of readability and maintainability. You need to write one struct for each handler which will represent the request and response data of that handler. This way maintaining consistency across all endpoints throughout the development process for fields, types, validation and serialization becomes joy, and a soft codebase compliance is enforced at each make build
.
Package structure
Let’s say you have resource “user” and actions create, read, update, delete on it. The rules and best practices combined needs you to define a central struct for holding the common dependencies of handlers:
type User struct {
JustTheCommonDeps any
TheOtherLayer any
OrTheDatabase any
OrARateLimiter any
YouWillSetThoseTho any
NotOurBusiness any
}
Then, you are expected to define your handlers with User type being the receiver:
func (u *User) Create(w http.ResponseWriter, r *http.Request)
func (u *User) Read(w http.ResponseWriter, r *http.Request)
func (u *User) Update(w http.ResponseWriter, r *http.Request)
func (u *User) Delete(w http.ResponseWriter, r *http.Request)
You probably will put each into different files. But make sure they match the http.HandlerFunc
signature. Gohandlers CLI recognizes handlers by the input and output parameter list. They either match at the character basis, or not. The receivers are okay to be empty, so function handlers are allowed. In fact, when you generate the helpers file on such directory with Go files that includes the combination of function and method handlers, you’ll notice there will be multiple listers as they are generated separately to match its receiver to handler’s receiver. More on that later in the Listers page.
File structure
Gohandlers expects one handler per file and its optional request and response binding struct declarations. The names for binding types should take the handler name as prefix and only add Request
or Response
to their ends. Gohandler will check if the handler body mentions the binding types. Complying with both rules is required for making Gohandlers consider the handler and its bindings for the code generation. Notice this file below contains 3 declarations. The handler name is Create
and its bindings CreateRequest
and CreateResponse
. Verb handler names and resource receivers are as short as it gets. This naming pattern is highly encouraged to be considered when naming handlers.
type CreateRequest struct {
Name types.PetName `json:"name"`
Tag types.PetTag `json:"tag"`
}
type CreateResponse struct {
ID string `json:"id"`
}
func (p *Pets) Create(w http.ResponseWriter, r *http.Request) {
_ := &CreateRequest{}
_ := &CreateResponse{}
}
Just like above, even for under construction handlers, the body should mention the binding types prior to the code generation. Or the code generation will skip binding types. Notice the request binding field types are custom. You will need to implement your types that supports serialization depending on the context (route, query or form body) and implements its custom field validation method. The former is not required when json
is used but even then field validators will be required. So, using core types for request bindings may generate code failing in compilation due to the missing methods.
Tagging fields
Tags tell Gohandlers which part of HTTP request/response the builders/parsers should look for reading or writing values. So you need to tag binding type fields with supported tags.
type ListRequest struct {
Limit types.ListLimit `query:"limit"` // optional
}
Each field should include one of the following:
Tag | Request | Response | Position |
---|---|---|---|
route |
A | NA | Header |
query |
A | NA | Header |
form |
A | NA | Body |
json |
A | A | Body |
Implement field serialization methods
When a type is used as a field type inside a binding type, it might need to implement a couple of interfaces. For types used as a field type for fields with route
tag, the type needs to implement Routier
interface below. This would allow request and response builders and parsers to perform their operations without cutting your hands off from implementing custom serialization methods per-type.
type Routier interface {
FromRoute(v string) error
ToRoute() (string, error)
}
Types used as a field type for query parameters via query
tag need to implement Querier
interface below. Compared to others, To
method needs to return 3 parameter. In addition to the standard encoded value and encoding error it is expected to return a middle one. Which represents the existence of value. If all query parameters returns false, the request builder will skip adding the ?
query section to the URL.
type Querier interface {
FromQuery(v string) error
ToQuery() (string, bool, error)
}
Types used as a field type for fields with form
tags, methods of Formier
interface is expected to serialize according to the x-www-form-urlencoded
. Opposed to the json
bodies, marshaling and unmarshaling form
bodies with Gohandlers provided methods doesn’t involve reflect
. But also they don’t support nested data structures.
type Formier interface {
FromForm(v string) error
ToForm() (string, error)
}
Implement field validators
To use the request validators generated by the validate
command, users are required to implent the FieldValidator
interface on every type used as a field type to a request response type:
type FieldValidator interface {
Validate() any
}
For example, if there is a UserId
type used as field type to a request binding, then the user needs to implement:
func (u UserId) Validate() any
Validate methods are forbidden to perform resource intensive operations like database accesses for their purpose, as they are solely exist for being called by request validators. Which are meant to be called for filtering invalid requests as cheaply and early as possible inside handlers, prior to resourceful operations. This only allows formal inspection of values. Thus, they are mostly expected to perform length, range or pattern checks.
Field validators return issues instead of error
s for both their technical and semantical meanings. That is because Go error
are meant to stay private to the server, often utilized for only internal purposes. At the other hand, validators are expected to return ready-to-serialization values for sending back to the client. This can be as basic as a string
value that is worded as an explanation to the user; or an error code that will be processed by the frontend app for localization. For fields with collection types, the return value can be a slice or map issues.
Beware that return values are constrained by the constraints of serializer that will be used inside the handler for request validation issues. For example, using JSON encoder to serialize request validator response will cause the output to miss any error
value returned by field validators.
Once the request validators generated and called by your app, the IDE warnings (via gopls
) for types with missing field validators will make the user notice problems at an instant, far before the app goes production.
Naming handlers
Gohandlers can infer the HTTP method of an handler just by looking to the verb at the beginning of handler name and/or the request binding body parameters. Any handler name starts
Method | Prefix |
---|---|
GET |
Get, Visit |
HEAD |
Head |
POST |
Post, Upload, Create |
PUT |
Put, Replace |
PATCH |
Patch, Update |
DELETE |
Delete, Remove |
CONNECT |
Connect |
OPTIONS |
Options |
TRACE |
Trace |
Annotate handlers
To set the HTTP method, endpoint path or both of an handler it is possible to use function doc-comments. To overwrite the method inferred from request binding body and handler name prefix with the selection of yours.
// POST /account
func (a *Api) CreateAccount(w http.ResponseWriter, r *http.Request) {
bq := &CreateAccountRequest{}
If an handler’s specified and inferred methods conflict then the code genration will provide a warning or an error. Such as in the below example the specified method is requiring a body, but the request binding type doesn’t contain a field with json
or form
tags.
type CreateAuthorizationForEventRequest struct {
EventId basics.String `route:"eid"`
}
// POST /event/{eid}/authorization
func (a *Api) CreateAuthorizationForEvent(w http.ResponseWriter, r *http.Request)
You can also limit or disable Gohandlers on the per-handler basis. Gohandlers can ignore selected handlers completely or only include them in the listers. The latter is suggested for endpoints where handling the request parsing, and/or validation involve special logic. Such as in multipart or stream request. To only list a handler, use the list
directive like below. This would cause Gohandlers to skip implementing request builder, parser, validator and response builder with writer on the binding types for that handler.
// gh:list
func (p Pets) UploadPhoto(w http.ResponseWriter, r *http.Request)
To instruct Gohandlers to completely ignore a handler you need to annotate the handler as such below.
// gh:ignore
func (p Pets) CreateStream(w http.ResponseWriter, r *http.Request)