Recipes
Short, self-contained solutions to common tasks. Each one can be copied into an application and adapted.
Health check
A route that answers every method, useful for load balancer probes:
app.Route("*", "/healthz", func(ctx *vox.Context, req *vox.Request[vox.NoBody], res *vox.Response[string]) {
res.Body = "ok"
})Request ID
Reuse the ID sent by the client or a proxy, or generate one. The ID is echoed in the response and stored in the context for handlers and loggers.
type requestIDKey struct{}
func requestID(ctx *vox.Context, req *vox.BaseRequest, res *vox.BaseResponse) {
id := req.Header.Get("X-Request-ID")
if id == "" {
id = rand.Text()
}
res.Header.Set("X-Request-ID", id)
ctx.Context = context.WithValue(ctx.Context, requestIDKey{}, id)
ctx.Next()
}
// RequestID returns the ID of the current request.
func RequestID(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey{}).(string)
return id
}rand.Text comes from crypto/rand.
CORS
Allow a browser application on another origin to call your API. Preflight requests are answered by the middleware and never reach a route.
func cors(origin string) vox.Handler {
return func(ctx *vox.Context, req *vox.BaseRequest, res *vox.BaseResponse) {
res.Header.Set("Access-Control-Allow-Origin", origin)
res.Header.Add("Vary", "Origin")
if req.Method == http.MethodOptions && req.Header.Get("Access-Control-Request-Method") != "" {
res.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE")
res.Header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
res.Header.Set("Access-Control-Max-Age", "86400")
res.Status = http.StatusNoContent
return
}
ctx.Next()
}
}
app.Use(cors("https://app.example.com"))Bearer token authentication
Reject requests without a valid token, and make the authenticated user available to handlers.
type userKey struct{}
func authenticate(lookup func(token string) (User, bool)) vox.Handler {
return func(ctx *vox.Context, req *vox.BaseRequest, res *vox.BaseResponse) {
token, ok := strings.CutPrefix(req.Header.Get("Authorization"), "Bearer ")
user, found := lookup(token)
if !ok || !found {
res.Header.Set("WWW-Authenticate", "Bearer")
res.Status = http.StatusUnauthorized
return
}
ctx.Context = context.WithValue(ctx.Context, userKey{}, user)
ctx.Next()
}
}
func me(ctx *vox.Context, req *vox.Request[vox.NoBody], res *vox.Response[User]) {
res.Body = ctx.Value(userKey{}).(User)
}Basic authentication
Protect an application with a single user name and password. The comparison is done in constant time.
func basicAuth(username, password string) vox.Handler {
return func(ctx *vox.Context, req *vox.BaseRequest, res *vox.BaseResponse) {
u, p, ok := req.BasicAuth()
userOK := subtle.ConstantTimeCompare([]byte(u), []byte(username)) == 1
passOK := subtle.ConstantTimeCompare([]byte(p), []byte(password)) == 1
if !ok || !userOK || !passOK {
res.Header.Set("WWW-Authenticate", `Basic realm="restricted"`)
res.Status = http.StatusUnauthorized
return
}
ctx.Next()
}
}Security headers
Headers set before ctx.Next() apply to every response, including errors and responses that are written directly, such as static files.
func securityHeaders(ctx *vox.Context, req *vox.BaseRequest, res *vox.BaseResponse) {
res.Header.Set("X-Content-Type-Options", "nosniff")
res.Header.Set("X-Frame-Options", "DENY")
res.Header.Set("Referrer-Policy", "no-referrer")
ctx.Next()
}Request timeout
Give every request a deadline. Handlers that pass ctx to their database and HTTP calls are interrupted when it expires.
func timeout(d time.Duration) vox.Handler {
return func(ctx *vox.Context, req *vox.BaseRequest, res *vox.BaseResponse) {
var cancel context.CancelFunc
ctx.Context, cancel = context.WithTimeout(ctx.Context, d)
defer cancel()
ctx.Next()
}
}
func report(ctx *vox.Context, req *vox.Request[vox.NoBody], res *vox.Response[Report]) {
result, err := buildReport(ctx)
if errors.Is(err, context.DeadlineExceeded) {
res.Status = http.StatusGatewayTimeout
return
}
if err != nil {
res.Status = http.StatusInternalServerError
return
}
res.Body = result
}File upload
Read a multipart upload in a NoBody handler and save it to disk. The size of the request is capped first, because the automatic limit only covers JSON bodies.
func upload(ctx *vox.Context, req *vox.Request[vox.NoBody], res *vox.Response[map[string]any]) {
req.Request.Body = http.MaxBytesReader(res.Writer, req.Request.Body, 10<<20)
file, header, err := req.FormFile("file")
if err != nil {
res.Status = http.StatusBadRequest
res.BaseResponse.Body = "a file field is required and must be at most 10 MiB"
return
}
defer file.Close()
// Never trust the client's file name as a path.
dst, err := os.Create(filepath.Join("uploads", filepath.Base(header.Filename)))
if err != nil {
res.Status = http.StatusInternalServerError
return
}
defer dst.Close()
size, err := io.Copy(dst, file)
if err != nil {
res.Status = http.StatusInternalServerError
return
}
res.Status = http.StatusCreated
res.Body = map[string]any{"name": header.Filename, "size": size}
}File download
Stream a file as an attachment. The file is closed after it has been sent.
func download(ctx *vox.Context, req *vox.Request[vox.NoBody], res *vox.Response[io.ReadCloser]) {
file, err := os.Open("reports/2026.csv")
if err != nil {
res.Status = http.StatusNotFound
return
}
res.Header.Set("Content-Type", "text/csv")
res.Header.Set("Content-Disposition", `attachment; filename="2026.csv"`)
res.Body = file
}To serve a whole directory, use the Static Files middleware.
Structured access log
The built-in access log has a fixed format. To log in your own format, turn it off and wrap the application at the net/http level, where the final status is visible:
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func (r *statusRecorder) Unwrap() http.ResponseWriter {
return r.ResponseWriter
}
func accessLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(recorder, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", recorder.status,
"duration", time.Since(start),
)
})
}
func main() {
app := vox.New()
app.SetConfig("logging:disable", "true")
// register middleware and routes
http.ListenAndServe("localhost:3000", accessLog(app))
}A Vox middleware is not the right place for this, because the default status is only filled in after all your middleware has returned.
More
These tasks are covered in the guide: