I’m currently developing a webservice in Go for a customer, where I need to write a lot of HTTP endpoints. Most of them have a very similar structure:
While the Echo framework already provides some useful helper to bind and plug in a validator, my function headers always hold the same boilerplate code:
type mgmtImportRequest struct {
Email string `json:"foo" validate:"required,email"`
}
// handleMgmtImport
func (hs *HTTPServer) handleMgmtImport(c echo.Context) error {
u := new(mgmtImportRequest)
if err := c.Bind(u); err != nil {
return c.String(http.StatusBadRequest, "bad request"
}
if err = c.Validate(u); err != nil {
return err
}
// [...]
}
While binding and validation can be combined in one call, I still do not like to have this at every single function call.
Looking at other (new-ish) frameworks in other languages, I like how the validation is already part of the route definition. For example, this is how a route definition in FastAPI can look like:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return item
Passing Item
here will bind and validate at once.
Or in JavaScript with fastify:
fastify.route({
method: 'GET',
url: '/',
schema: {
// request needs to have a querystring with a `name` parameter
querystring: {
type: 'object',
properties: {
name: { type: 'string'}
},
required: ['name'],
},
// the response needs to be an object with an `hello` property of type 'string'
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
},
// this function is executed for every request before the handler is executed
preHandler: async (request, reply) => {
// E.g. check authentication
},
handler: async (request, reply) => {
return { hello: 'world' }
}
})
So, how can we have those nice things in Go, too?
Let’s approach it with generics. We start with the example from the Echo docs:
package main
import (
"net/http"
"github.com/go-playground/validator"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type (
User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
CustomValidator struct {
validator *validator.Validate
}
)
// [1]
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
// Optionally, you could return the error to give each route more control over the status code
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return nil
}
func main() {
e := echo.New()
e.Validator = &CustomValidator{validator: validator.New()} // [2]
e.POST("/users", func(c echo.Context) (err error) {
u := new(User)
// [3]
if err = c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// [4]
if err = c.Validate(u); err != nil {
return err
}
// [5]
return c.JSON(http.StatusOK, u)
})
e.Logger.Fatal(e.Start(":1323"))
}
To summarize what is happening here:
Validate
on the
echo.Context
. This will directly call into the validator
package.json:"foo"
to define where request data is coming from.http.StatusBadRequest
with an
validation error message).The first step is now to move binding and validation into some kind of middleware. Echo has ways to implement middleware, but we will go a different path here so we can later on also pass a type. What I am aiming for is a route definition which looks like this:
e := echo.New()
e.POST("/users", validated[User](users))
This way, I could annotate my route handling function with a (validation) type it has to adhere to.
Let’s create the binding/validation first:
package main
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
func users(c echo.Context, u User) (err error) {
return c.JSON(http.StatusOK, u)
}
func main() {
e := echo.New()
e.POST("/users", validatedUser(users))
e.Logger.Fatal(e.Start(":1323"))
}
func validatedUser(h func(echo.Context, User) error) echo.HandlerFunc {
var validate *validator.Validate
validate = validator.New(validator.WithRequiredStructEnabled())
return func(c echo.Context) error { var u User
if err := c.Bind(&u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := validate.Struct(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return h(c, u)
}
}
Now we can generalise the validation function. It will take a type T
and use
this type to let validator
handle validation. Finally, it will pass this data
to the handler function:
func validated[T any](h func(c echo.Context, t T) error) echo.HandlerFunc {
var validate *validator.Validate
validate = validator.New(validator.WithRequiredStructEnabled())
return func(c echo.Context) error {
var t T
if err := c.Bind(&t); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := validate.Struct(t); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return h(c, t)
}
}
This function will now bind, validate and only if this succeeds call the final handling function with the parsed data.
Setting up routes now looks like this:
e.POST("/users", validated[User](users))
e.POST("/post", validated[Post](posts))
And because Go can infer the generic type here, we can also omit it:
e.POST("/users", validated(users))
Here is a full working example:
package main
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
type Post struct {
Name string `json:"name" validate:"required"`
}
func users(c echo.Context, u User) (err error) {
return c.JSON(http.StatusOK, u)
}
func posts(c echo.Context, p Post) (err error) {
return c.JSON(http.StatusOK, p)
}
func main() {
e := echo.New()
e.POST("/users", validated(users))
e.POST("/post", validated(posts))
e.Logger.Fatal(e.Start(":1323"))
}
func validated[T any](h func(c echo.Context, t T) error) echo.HandlerFunc {
var validate *validator.Validate
validate = validator.New(validator.WithRequiredStructEnabled())
return func(c echo.Context) error {
var t T
if err := c.Bind(&t); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := validate.Struct(t); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return h(c, t)
}
}
This approach can of course also be used when using HTTP handler with the stdlib.