The peaceful sound of minimal Go.
Aarv is a lightweight, zero-dependency Go web framework built on top of net/http.
Inspired by .NET Minimal API (type-safe binding, fluent registration) and Fastify (plugins, lifecycle hooks, encapsulation).
- Zero external dependencies in core — pure Go stdlib
- Go 1.22+ ServeMux routing with method patterns, path params, and wildcards
- Type-safe request binding via Go generics (
Bind[Req, Res]) - Multi-source binding — path params, query, headers, cookies, body, form — all via struct tags
- Built-in validation engine — struct tag rules, pre-computed at registration, zero-alloc hot path
- Fastify-style lifecycle hooks — OnRequest, PreHandler, OnResponse, OnSend, OnError, and more
- Scoped plugin system — encapsulated plugins with decorators, nested registration
- Middleware compatible — standard
func(http.Handler) http.Handlerworks out of the box - Pooled Context —
sync.Poolrecycled context for minimal GC pressure - Pluggable JSON codec — swap
encoding/jsonfor segmentio, sonic, or json/v2 - Graceful shutdown with signal handling and drain timeout
- TLS / HTTP/2 / mTLS — production-ready with sensible defaults
package main
import "github.com/nilshah80/aarv"
func main() {
app := aarv.New()
app.Get("/hello", func(c *aarv.Context) error {
return c.JSON(200, map[string]string{"message": "hello, world"})
})
app.Listen(":8080")
}Concrete examples live under examples/:
examples/hello— minimal routes,Bind, route groupsexamples/rest-crud— CRUD-style app structureexamples/hooks— full lifecycle hooks includingPreRouting,PreParsing,PreValidation,PreHandler,OnErrorexamples/route-groups— nested route groups with scoped middlewareexamples/binding— multi-source binding across path, query, header, and JSON bodyexamples/error-handling— custom error handler plusOnErrorexamples/custom-middleware— stdlib-only, native-only, and dual-registered custom middlewareexamples/middleware-bridge— stdlib middleware usingr.WithContext(...)with Aarv compatibilityexamples/json-logger— standard logger plus debug, production-safe, and minimalverboselogpresetsexamples/encrypt— AES-GCM request/response encryptionexamples/performance-profile— bridge-off plus fast JSON codec for throughput-oriented servicesexamples/custom-plugin— decorators, dependencies, plugin-scoped routes, dual middleware registrationexamples/auth— JWT, API key, and session authexamples/database— repository-style app with typed handlersexamples/fileserverandexamples/streaming— file and stream responses
type CreateUserReq struct {
Name string `json:"name" validate:"required,min=2"`
Email string `json:"email" validate:"required,email"`
}
type CreateUserRes struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
app.Post("/users", aarv.Bind(func(c *aarv.Context, req CreateUserReq) (CreateUserRes, error) {
// req is already parsed, validated, and typed
return CreateUserRes{ID: "123", Name: req.Name, Email: req.Email}, nil
}))type GetOrderReq struct {
UserID string `param:"userId"`
OrderID int `param:"orderId"`
Fields string `query:"fields" default:"*"`
Token string `header:"X-Api-Key"`
}
app.Get("/users/{userId}/orders/{orderId}", aarv.BindReq(func(c *aarv.Context, req GetOrderReq) error {
// req.UserID from path, req.Fields from query, req.Token from header
return c.JSON(200, getOrder(req))
}))app.Use(aarv.Recovery(), aarv.Logger())
app.Group("/api/v1", func(g *aarv.RouteGroup) {
g.Use(authMiddleware)
g.Get("/users", listUsers)
g.Post("/users", aarv.Bind(createUser))
g.Group("/admin", func(ag *aarv.RouteGroup) {
ag.Use(adminOnly)
ag.Delete("/users/{id}", deleteUser)
})
})Aarv supports two middleware styles:
- stdlib-compatible:
func(http.Handler) http.Handler - Aarv-native:
func(next aarv.HandlerFunc) aarv.HandlerFunc
Stdlib middleware is the compatibility path. It works with ordinary Go middleware and preserves Aarv context across r.WithContext(...) clones by default.
Aarv-native middleware is the faster path. Use aarv.WrapMiddleware(...) for custom middleware that needs *aarv.Context directly:
app.Use(aarv.WrapMiddleware(func(next aarv.HandlerFunc) aarv.HandlerFunc {
return func(c *aarv.Context) error {
c.SetHeader("X-Trace", "on")
return next(c)
}
}))For middleware-heavy services that never rely on Aarv context recovery from cloned requests, WithRequestContextBridge(false) remains an opt-in fast mode.
app.AddHook(aarv.OnRequest, func(c *aarv.Context) error {
c.Set("startTime", time.Now())
return nil
})
app.AddHook(aarv.OnResponse, func(c *aarv.Context) error {
start, _ := aarv.GetTyped[time.Time](c, "startTime")
c.Logger().Info("request completed", "latency", time.Since(start))
return nil
})app.Register(aarv.PluginFunc(func(p *aarv.PluginContext) error {
p.Decorate("db", connectDB())
p.Get("/health", healthCheck)
return nil
}))import "github.com/nilshah80/aarv/codec/segmentio"
app := aarv.New(aarv.WithCodec(segmentio.New()))The core framework stays zero-dependency and production-oriented. The requestid plugin is opt-in, but Aarv preserves framework context across raw r.WithContext(...) middleware clones by default so standard Go middleware keeps working.
WithRequestContextBridge(false) is an opt-in mode for middleware-heavy services that never rely on aarv.FromRequest(...) after cloning requests with r.WithContext(...).
Use it when:
- your middleware stack does not need Aarv context recovery from cloned requests
- you want the leanest compatibility tradeoff for fully controlled middleware stacks
Do not use it when:
- standard middleware calls
r.WithContext(...)and downstream code expectsaarv.FromRequest(...)to still work on the cloned request - you need the default compatibility behavior across mixed stdlib middleware
Example:
app := aarv.New(
aarv.WithBanner(false),
aarv.WithRequestContextBridge(false),
)Not all middleware have the same memory and streaming behavior. For production workloads, the important split is:
- Stream-safe: plain routing, native middleware, most header-only middleware,
bodylimit,recover,requestid,secure,cors - Bounded-buffer:
compressbuffers until the compression threshold decision is made;verboselogwith body logging buffers captured request bytes and response bytes up toMaxBodySize - Full-buffer:
etagbuffers the full response body;encryptfully reads encrypted request bodies and buffers full response bodies for encryption
Use the full-buffer plugins only on routes where payload size is bounded and intentional. For large file, streaming, or long-lived responses, keep those routes on stream-safe middleware only.
For services where raw throughput matters more than maximum stdlib middleware compatibility, the measured fast-path profile is:
import (
"github.com/nilshah80/aarv"
"github.com/nilshah80/aarv/codec/segmentio"
)
app := aarv.New(
aarv.WithBanner(false),
aarv.WithRequestContextBridge(false),
aarv.WithCodec(segmentio.New()),
)Why this profile:
WithRequestContextBridge(false)removes the cloned-request compatibility overhead for stdlib middleware stacks that never needaarv.FromRequest(...)afterr.WithContext(...).segmentio.New()is currently the best measured JSON codec in the local benchmark harness for bothc.JSON(...)and bind/decode workloads.
Current local benchmark signal from tests/benchmark:
- stdlib middleware chain with bridge on: about
219 ns/op,856 B/op,5 allocs/op - stdlib middleware chain with bridge off: about
177-181 ns/op,512 B/op,3 allocs/op - default stdlib JSON response path: about
399-408 ns/op,945 B/op,9 allocs/op - segmentio JSON response path: about
356-364 ns/op,884 B/op,7 allocs/op - default stdlib bind path: about
1010-1021 ns/op,1308 B/op,16 allocs/op - segmentio bind path: about
642-658 ns/op,1085-1087 B/op,12 allocs/op
These numbers are machine- and workload-dependent, but the ordering has been stable in the benchmark harness.
See examples/performance-profile for a runnable setup using this profile.
Request → Global Middleware → ServeMux Route Match → Group Middleware → Hooks → Bind → Validate → Handler → Response
↕ sync.Pool Context ↕ Pluggable Codec ↕ Buffered Writer ↕ Error Handler
- Minimal — thin layer over
net/http, not a replacement - Zero-dep core — everything in the core uses only the Go standard library
- Fast — pooled contexts, pre-computed binders/validators, pre-built middleware chains
- Familiar — standard middleware signature, standard handler patterns, nothing magical
- Pluggable — swap JSON codec, validator, error handler, logger — all via interfaces
MIT