A lightweight, secure-by-default HTTP framework for Go. Built on net/http with zero external dependencies.
Built on stdlib, not instead of it. zerohttp builds on Go's net/http rather than replacing it, so your handlers stay standard http.HandlerFunc and work with existing middleware and tooling.
Secure by default. Sensible security headers, request body limits, panic recovery, and request IDs are applied automatically for every request.
Zero dependencies. Single module, standard library only, so your service stays lean and easy to upgrade.
Handler errors that make sense. Handlers return error, and RFC 9457 Problem Details responses are generated for you automatically.
- Zero dependencies - Single module, no external deps
- Secure by default - Security headers, body limits, recovery, request IDs enabled automatically
- Standard library foundation - Built on
net/http, works with anyhttp.Handlermiddleware - Handler errors - Return
error, get proper HTTP responses automatically - Request binding - JSON, form, multipart, and query params to structs with struct tags
- Validation - Built-in struct validation with 40+ validators
- Problem Details - RFC 9457 compliant error responses
- Middleware - CORS, rate limiting, auth, circuit breaker, and more
- Metrics - Prometheus-compatible metrics at
/metrics - Lifecycle hooks - Pre/post startup and shutdown hooks
- Pluggable - Bring your own validator, tracer, HTTP/3, WebSocket, SSE
go get github.com/alexferl/zerohttpRequires Go 1.25 or later.
package main
import (
"log"
"net/http"
zh "github.com/alexferl/zerohttp"
)
func main() {
app := zh.New()
app.GET("/hello/{name}", zh.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
name := zh.Param(r, "name")
return zh.Render.JSON(w, http.StatusOK, zh.M{"message": "Hello, " + name + "!"})
}))
log.Fatal(app.Start())
}go run main.go
curl http://localhost:8080/hello/world
{"message":"Hello, world!"}See the examples/ directory for more complete examples.
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
}
app.POST("/users", zh.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
var req CreateUserRequest
if err := zh.BindAndValidate(r, &req); err != nil {
return err // Automatic Problem Details response
}
// Process valid request...
return zh.R.JSON(w, http.StatusCreated, req)
}))app.Group(func(api zh.Router) {
api.Use(basicauth.New(basicauth.Config{
Credentials: map[string]string{"admin": "secret"},
}))
api.GET("/admin/dashboard", dashboardHandler)
})type SearchRequest struct {
Query string `query:"q" validate:"required"`
Limit int `query:"limit" validate:"max=100"`
}
app.GET("/search", zh.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
var req SearchRequest
if err := zh.BindAndValidate(r, &req); err != nil {
return err
}
return zh.R.JSON(w, http.StatusOK, zh.M{"results": []string{}})
}))app.GET("/users/{id}", zh.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
id := zh.Param(r, "id")
user, err := db.GetUser(id)
if err != nil {
problem := zh.NewProblemDetail(http.StatusNotFound, "user not found")
return zh.R.ProblemDetail(w, problem)
}
return zh.R.JSON(w, http.StatusOK, user)
}))app.GET("/health", zh.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return zh.R.Text(w, http.StatusOK, "healthy")
}))
app.GET("/docs", zh.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return zh.R.Redirect(w, r, "https://pkg.go.dev/github.com/alexferl/zerohttp", http.StatusFound)
}))zerohttp uses struct-based configuration:
app := zh.New(zh.Config{
Addr: ":8080",
TLS: zh.TLSConfig{
Addr: ":8443",
CertFile: "cert.pem",
KeyFile: "key.pem",
},
RequestBodySize: requestbodysize.Config{
MaxBytes: 5 * 1024 * 1024, // 5MB
},
})These middlewares are applied automatically:
- Request ID - Unique IDs for tracing
- Panic Recovery - Graceful panic handling with stack traces
- Request Body Size Limits - DoS protection (1MB default)
- Security Headers - CSP, HSTS, X-Frame-Options, etc.
- Request Logging - Structured request/response logging
Disable or customize via zh.Config.
The storage package provides a shared interface for middleware storage backends. Implement storage.Storage once (for Redis, PostgreSQL, etc.) and reuse it across multiple middlewares via adapters:
import (
"github.com/alexferl/zerohttp/middleware/cache"
"github.com/alexferl/zerohttp/middleware/idempotency"
"github.com/alexferl/zerohttp/storage"
)
// Implement storage.Storage and storage.Locker in your own package
type MyRedis struct { /* ... */ }
func (r *MyRedis) Get(ctx context.Context, key string) ([]byte, bool, error) { /* ... */ }
func (r *MyRedis) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error { /* ... */ }
func (r *MyRedis) Delete(ctx context.Context, key string) error { /* ... */ }
func (r *MyRedis) Lock(ctx context.Context, key string, ttl time.Duration) (bool, error) { /* ... */ }
func (r *MyRedis) Unlock(ctx context.Context, key string) error { /* ... */ }
// One backend, multiple middlewares
redis := &MyRedis{}
app.Use(cache.New(cache.Config{
Store: cache.NewStorageAdapter(redis),
}))
idempotencyStore, _ := idempotency.NewStorageAdapter(redis)
app.Use(idempotency.New(idempotency.Config{
Store: idempotencyStore,
}))The zhtest package provides fluent test helpers:
func TestGetUser(t *testing.T) {
app := setupRouter()
req := zhtest.NewRequest(http.MethodGet, "/users/123").Build()
w := zhtest.Serve(app, req)
zhtest.AssertWith(t, w).Status(http.StatusOK).JSONPathEqual("name", "John")
}MIT License - see LICENSE for details.