When I tell people about Marten, I often hear: "Isn't that just like Chi?"
Short answer: No.
Long answer: Let me show you why these frameworks, despite both being minimal Go web routers, are actually solving different problems.
The 30-Second Pitch
Both Marten and Chi are:
- β Zero-dependency (pure stdlib)
- β Fast radix tree routing
- β Middleware support
- β Lightweight and performant
But that's where the similarities end. Let's dig into what actually matters.
The Handler Signature: Where Everything Diverges
This is the fundamental difference that changes everything:
Chi: Pure Stdlib
func handler(w http.ResponseWriter, r *http.Request) {
user := User{Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
json.NewEncoder(w).Encode(user)
}
Marten: Framework Abstraction
func handler(c *marten.Ctx) error {
user := User{Name: "Alice"}
return c.OK(user)
}
This isn't just syntax sugar - it's a completely different programming model. Let me show you why.
Error Handling: Manual vs Automatic
Chi: You Handle Everything
func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := db.GetUser(id)
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{
"error": err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
json.NewEncoder(w).Encode(user)
}
Marten: Framework Handles Errors
func getUser(c *marten.Ctx) error {
id := c.Param("id")
user, err := db.GetUser(id)
if err != nil {
return err // Framework catches this
}
return c.OK(user)
}
// In main.go
app.OnError(func(c *marten.Ctx, err error) {
log.Printf("Error: %v", err)
c.JSON(500, marten.E(err.Error()))
})
The difference: Chi gives you control. Marten gives you consistency.
Safety: Guard Rails vs. Freedom
Route Conflict Detection
Marten panics at startup on conflicting routes:
app.GET("/users/:id", handler1)
app.GET("/users/:userId", handler2) // PANIC: route conflict!
Chi silently allows this (last route wins):
r.Get("/users/{id}", handler1)
r.Get("/users/{userId}", handler2) // No error, handler2 wins
Double-Write Prevention
Marten tracks response state:
func handler(c *marten.Ctx) error {
c.Status(200)
c.Status(404) // Ignored - already written
return nil
}
Chi lets you shoot yourself in the foot:
func handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.WriteHeader(404) // Runtime error!
}
Performance: Different Approaches
Marten: Context Pooling
// Internal implementation
app.pool = sync.Pool{
New: func() any {
return &Ctx{
params: make(map[string]string),
store: make(map[string]any),
}
},
}
Marten reuses context objects with sync.Pool, reducing allocations per request.
Chi: No Pooling
Chi allocates fresh contexts every request. It's still fast, but has different memory characteristics.
Benchmark difference: Marten typically shows 10-15% fewer allocations in high-throughput scenarios.
The Type Incompatibility Problem
Here's something crucial: You cannot mix Chi and Marten middleware.
// Chi middleware
func chiLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ...
})
}
// Marten middleware
func martenLogger(next marten.Handler) marten.Handler {
return func(c *marten.Ctx) error {
// ...
}
}
These are fundamentally incompatible types. You'd need adapters to convert between them.
The Motorcycle vs Sports Car Analogy
Chi = Motorcycle ποΈ
- Raw, direct control
- You feel every operation
- Manual everything
- Experienced riders love it
- "Don't help me, I know what I'm doing"
Marten = Sports Car ποΈ
- Fast with modern conveniences
- Safety features built-in
- Automatic transmission with manual mode
- Assists when needed, stays out of your way otherwise
- "Help me go fast safely"
Real-World Code Comparison
Let's build a simple CRUD endpoint:
Chi Version
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON"})
return
}
if err := validate(user); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
if err := db.Create(&user); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": "database error"})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201)
json.NewEncoder(w).Encode(user)
}
Marten Version
func createUser(c *marten.Ctx) error {
var user User
if err := c.Bind(&user); err != nil {
return c.BadRequest("invalid JSON")
}
if err := validate(user); err != nil {
return c.BadRequest(err.Error())
}
if err := db.Create(&user); err != nil {
return err // Caught by OnError handler
}
return c.Created(user)
}
40% less code. Same functionality. More readable.
When to Choose Which?
Choose Chi If:
- β You want maximum stdlib compatibility
- β
You're migrating from
http.ServeMux - β You prefer explicit control over everything
- β You want to build your own abstractions
- β You need the mature Chi ecosystem
Choose Marten If:
- β You want Echo/Fiber ergonomics in pure Go
- β You prefer safety features (conflict detection, write protection)
- β You want less boilerplate
- β You're building REST APIs quickly
- β You value consistent error handling
The Ecosystem Question
Chi's advantage: 13+ years of community packages, battle-tested in production.
Marten's advantage: 13 built-in middleware that cover 90% of use cases:
- Logger, Recover, CORS, RateLimit
- BasicAuth, Compress, ETag, NoCache
- Timeout, BodyLimit, Secure, RequestID
You trade ecosystem breadth for batteries-included convenience.
Migration Path
Chi β Marten
Requires rewriting handlers (signature change), but middleware concepts translate directly:
// Chi
r.Use(chiMiddleware)
r.Get("/users", getUsers)
// Marten equivalent
app.Use(martenMiddleware)
app.GET("/users", getUsers)
Marten β Chi
Similar effort - handler signatures must change, but routing logic stays the same.
Neither migration is trivial due to the type incompatibility.
My Take
After building with both:
Chi is the right choice when you want to stay close to the stdlib and have full control. It's a routing library that doesn't impose opinions.
Marten is the right choice when you want to ship features quickly with modern conveniences. It's an opinionated framework that reduces boilerplate.
They're not competitors - they're solving different problems:
- Chi: "I want routing and nothing else"
- Marten: "I want a complete foundation for REST APIs"
Try It Yourself
Chi
go get -u github.com/go-chi/chi/v5
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Chi"))
})
http.ListenAndServe(":3000", r)
Marten
go get github.com/gomarten/marten
app := marten.New()
app.Use(middleware.Logger)
app.GET("/", func(c *marten.Ctx) error {
return c.Text(200, "Hello Gomarten")
})
app.Run(":3000")
Conclusion
Chi and Marten are NOT the same.
They share superficial similarities but have fundamentally different:
- Handler signatures (incompatible types)
- Programming models (manual vs automatic)
- Safety guarantees (permissive vs protective)
- Performance characteristics (no pooling vs pooling)
Pick the one that matches your philosophy:
- ποΈ Chi for control and stdlib purity
- ποΈ Marten for speed and convenience
Both are excellent tools. Neither is "better" - they're just different.
What's your take? Are you Team Chi or Team Marten? Let me know in the comments! π
Marten is open source and actively maintained. Check it out at github.com/gomarten/marten
Top comments (0)