A tiny, fast HTTP middleware for creating JSON APIs with automatic input validation using Go generics.
go get github.com/xeoncross/midmid provides a generic Handler[T] function that wraps your business logic into a standard http.Handler. It automatically handles JSON decoding, struct validation, and error responses, allowing you to focus on your application logic.
The core function is Handler[T], which takes your handler plus any number of optional overrides and returns an http.Handler:
func Handler[T any](
handler HandlerFunc[T], // Your business logic
opts ...Option[T], // Optional overrides (see Options below)
) http.HandlerWith no options, Handler decodes JSON with JSONDecoder, validates with StructValidator, and reports errors with JSONErrorHandler — the three defaults every endpoint needs, without having to name them.
Your handler function must match the HandlerFunc[T] signature:
type HandlerFunc[T any] func(input T) (error, any)- input: The hydrated input struct (decoded from JSON and validated)
- returns: An error (if something went wrong) and any response value (encoded as JSON)
package main
import (
"net/http"
"github.com/xeoncross/mid"
)
// Define your input struct with validation tags
type CreateUserInput struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
// Define your response struct
type CreateUserResponse struct {
ID int `json:"id"`
Status string `json:"status"`
}
// Your handler logic
func createUser(input CreateUserInput) (error, any) {
// 'input' is auto-populated and passed validation
// ... business logic here ...
return nil, CreateUserResponse{
ID: 123,
Status: "created",
}
}
func main() {
mux := http.NewServeMux()
// Register the endpoint - decode, validate, and error handling all
// default to the package helpers
mux.Handle("/users", mid.Handler(createUser))
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
server.ListenAndServe()
}Each default can be overridden individually by passing an Option[T] to Handler. Unlisted overrides keep their default.
// Override just the validator
mux.Handle("/users", mid.Handler(createUser, mid.WithValidator(myValidator)))
// Override several at once
mux.Handle("/users", mid.Handler(createUser,
mid.WithDecoder(myDecoder),
mid.WithValidator(myValidator),
))| Option | Overrides | Signature |
|---|---|---|
WithDecoder |
JSONDecoder |
func WithDecoder[T any](d Decoder[*T]) Option[T] |
WithValidator |
StructValidator |
func WithValidator[T any](v Validator[T]) Option[T] |
WithErrorHandler |
JSONErrorHandler |
func WithErrorHandler[T any](e ErrorHandler[T]) Option[T] |
WithDecoder and WithValidator infer T from the function you pass in, so no type argument is needed. ErrorHandler[T] doesn't use T in its own signature, so when WithErrorHandler is the only option on a call, Go can't infer it from context and you need to spell it out:
mux.Handle("/users", mid.Handler(createUser, mid.WithErrorHandler[CreateUserInput](myErrorHandler)))Decodes the request body into your input struct. Handles common JSON errors and returns appropriate error messages.
type Decoder[T any] func(w http.ResponseWriter, r *http.Request, input T) boolReturns false if decoding fails (error response is automatically written to the client).
Validates your input struct using go-playground/validator.
type Validator[T any] func(w http.ResponseWriter, r *http.Request, input T) boolReturns false if validation fails (validation errors are automatically returned to the client as JSON).
Default error handler that returns errors in a consistent JSON format.
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)Default response format:
{
"error": "error message here"
}This package uses go-playground/validator for struct validation. Validation rules are defined using struct tags.
-
Global Validator Instance: The package maintains a global
ValidatorInstance(*validator.Validate) that is shared across all validation calls. This instance is lazily initialized on first use. -
Validation Tags: Use the
validatestruct tag to define validation rules:
type Input struct {
Name string `validate:"required"`
Email string `validate:"required,email"`
Age int `validate:"gte=0,lte=150"`
Role string `validate:"oneof=admin user guest"`
Tags []string `validate:"min=1"`
Score float64 `validate:"gte=0,lte=100"`
}-
Validation Errors: When validation fails, the raw
validator.ValidationErrorsslice is returned as JSON, allowing clients to inspect which fields failed validation. -
Custom Validators: You can register custom validation functions on
ValidatorInstancebefore first use:
mid.ValidatorInstance = validator.New()
mid.ValidatorInstance.RegisterValidation("custom_rule", myCustomValidator)| Tag | Description |
|---|---|
required |
Field must be set and non-empty |
email |
Field must be a valid email address |
min=N |
Field must be >= N |
max=N |
Field must be <= N |
gte=N |
Field must be >= N (for numbers) |
lte=N |
Field must be <= N (for numbers) |
oneof=x y z |
Field must be one of the specified values |
url |
Field must be a valid URL |
len=N |
Field must have exactly length N |
For a complete list of validation tags, see the go-playground/validator documentation.
All components are designed to be replaceable. You can provide your own decoder, validator, or error handler by implementing the corresponding type:
// Custom decoder example
func myDecoder[T any](w http.ResponseWriter, r *http.Request, input *T) bool {
// Custom decoding logic
return true
}
// Custom validator example
func myValidator[T any](w http.ResponseWriter, r *http.Request, input T) bool {
// Custom validation logic
return true
}
// Usage
mid.Handler(myHandler, mid.WithDecoder(myDecoder), mid.WithValidator(myValidator))MIT