An Express-inspired, idiomatic Go API framework built on net/http.
Zinc gives you expressive routing, middleware chains, request binding, response helpers, rendering, and a practical standard-library shape — without forking away from net/http. Handlers are one func(*zinc.Context) error, stdlib handlers mount cleanly, and deployment stays ordinary Go.
- Express-style routes with
:paramand*wildcard - Route groups, prefix middleware, and route metadata
- Binding helpers for path, query, headers, JSON, XML, and forms
- Response helpers for JSON, XML, HTML, streams, redirects, files, and rendering
- First-party template renderer for
html/templateandtext/template - Static/file serving and stdlib interop via
Mount,Wrap, andWrapFunc - Explicit startup and shutdown with
Listen,Serve, andShutdown - First-party middleware and a small in-memory jobs add-on in one module
go get github.com/0mjs/zincRequires Go 1.25 or newer.
package main
import (
"github.com/0mjs/zinc"
"github.com/0mjs/zinc/middleware"
)
func main() {
app := zinc.New()
app.Use(middleware.RequestLogger())
app.Get("/", "Hello, world!") // Shorthand
app.Get("/greet", func(c *zinc.Context) error {
return c.JSON(zinc.Map{
"greeting": "Hello, world!",
})
})
api := app.Group("/api")
api.Get("/health", func(c *zinc.Context) error {
return c.String("ok")
})
app.Listen()
}app.Use(middleware.RequestLogger())
app.UsePrefix("/api", authMiddleware)
app.Route("/api", func(api *zinc.Group) {
api.Get("/users/:id", showUser)
api.Post("/users", createUser)
})See the routing guide for groups, parameters, method shortcuts, and named routes.
type CreateUserInput struct {
TeamID int `path:"teamID"`
Page int `query:"page"`
Name string `json:"name"`
Auth string `header:"x-auth"`
}
app.Post("/teams/:teamID/users", func(c *zinc.Context) error {
var input CreateUserInput
if err := c.Bind().All(&input); err != nil {
return err
}
return c.Status(zinc.StatusCreated).JSON(input)
})c.Bind() covers path, query, header, JSON, XML, and form inputs. c.JSON, c.XML, c.String, c.Stream, c.File, c.Redirect, and c.Render cover the response side.
app := zinc.NewWithConfig(zinc.Config{
ServerHeader: "zinc/example",
CaseSensitive: true,
StrictRouting: true,
AutoHead: true,
AutoOptions: true,
HandleMethodNotAllowed: true,
BodyLimit: 8 << 20,
ProxyHeader: zinc.HeaderXForwardedFor,
TrustedProxies: []string{"10.0.0.1"},
})Config also takes a custom RequestBinder, Validator, Renderer, JSONCodec, and ErrorHandler:
views := template.Must(template.ParseGlob("templates/*.html"))
app := zinc.NewWithConfig(zinc.Config{
Renderer: zinc.NewHTMLTemplateRenderer(
views,
zinc.WithTemplateSuffixes(".html", ".tmpl"),
),
})
app.Get("/dashboard", func(c *zinc.Context) error {
return c.Render("dashboard", zinc.Map{"Title": "Overview"})
})All middleware lives under one package: github.com/0mjs/zinc/middleware.
import (
"log"
"os"
"time"
"github.com/0mjs/zinc"
"github.com/0mjs/zinc/middleware"
jwt "github.com/golang-jwt/jwt/v5"
)
app.Use(middleware.Recover())
app.Use(middleware.RequestID())
app.Use(middleware.CORS("https://app.example.com"))
app.Use(middleware.Decompress())
app.Use(middleware.Gzip())
app.Use(middleware.MethodOverride())
app.Use(middleware.Secure())
app.Use(middleware.TrailingSlash())
app.Use(middleware.KeyAuth(middleware.KeyAuthStatic(os.Getenv("API_KEY"))))
app.Use(middleware.Prometheus())
app.Get("/metrics", middleware.PrometheusHandler())
admin := app.Group("/admin")
admin.Use(middleware.BodyLimit(256 * middleware.KB))
admin.Use(middleware.ContextTimeout(250 * time.Millisecond))
app.Use(middleware.BodyDump(func(c *zinc.Context, snapshot middleware.BodyDumpSnapshot) {
log.Printf("%s %s -> %d", snapshot.Method, snapshot.Path, snapshot.Status)
}))
app.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
ExposeHeader: zinc.HeaderXCSRFToken,
}))
app.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{
Validator: middleware.BasicAuthStatic("admin", os.Getenv("ADMIN_PASSWORD")),
}))
app.Use(middleware.JWTWithConfig(middleware.JWTConfig{
KeyFunc: func(*zinc.Context, *jwt.Token) (any, error) {
return []byte("secret"), nil
},
}))Covered: BasicAuth, BodyDump, BodyLimit, CasbinAuth, ContextTimeout, CORS, CSRF, Decompress, Gzip, Jaeger, JWT, KeyAuth, MethodOverride, Pprof, Prometheus, Proxy, RateLimiter, Recover, Redirect, RequestID, RequestLogger, Rewrite, Secure, Session, Static, and TrailingSlash. Each has a full page under Middleware.
Zinc also ships a small first-party jobs add-on: github.com/0mjs/zinc/jobs.
queue := jobs.NewWithConfig(jobs.Config{
DefaultMaxAttempts: 3,
Backoff: jobs.ExponentialBackoff(time.Second, time.Minute),
})
if _, err := queue.Cron("log.hey", "10s", func(ctx context.Context) error {
log.Println("Hey!")
return nil
}); err != nil {
log.Fatal(err)
}
if err := queue.Handle("email.send", func(ctx context.Context, job jobs.Job) error {
var payload SendEmail
if err := job.Decode(&payload); err != nil {
return err
}
return mailer.Send(ctx, payload.To, payload.Subject)
}); err != nil {
log.Fatal(err)
}
runner, err := queue.Start(context.Background(), 4)
if err != nil {
log.Fatal(err)
}
defer runner.Stop(context.Background())
if _, err := queue.Enqueue(context.Background(), "email.send", SendEmail{
To: "sam@example.com",
Subject: "Welcome",
}); err != nil {
log.Fatal(err)
}
if _, err := queue.Schedule("reports.daily", "0 9 * * mon-fri", DailyReport{}); err != nil {
log.Fatal(err)
}The initial backend is in-memory and supports workers, delayed jobs, retries, failed-job inspection, and cron-style schedules. Use Cron for Nest-like scheduled function declarations, and drop down to Handle plus Schedule when the job needs a payload. Durable Postgres and Redis adapters can build on the same API.
Peer-only snapshot (Apple M1 Pro, darwin/arm64, rerun 2026-03-18): Zinc wins 65/85 rows overall against Gin, Echo, and Chi — 65/77 excluding throughput. Full tables and remaining gaps live in BENCKMARKS.md.
| Benchmark | Zinc | Gin | Echo | Chi | Winner |
|---|---|---|---|---|---|
HelloWorld |
61.82 ns |
88.21 ns |
127.2 ns |
178.8 ns |
Zinc |
APIHappyPath |
1.25 µs |
3.02 µs |
2.19 µs |
1.66 µs |
Zinc |
APIBindJSONHappyPath |
2.41 µs |
4.41 µs |
2.52 µs |
2.81 µs |
Zinc |
StaticFileHit |
15.64 µs |
32.85 µs |
26.95 µs |
15.97 µs |
Zinc |
RouteRegistrationStatic |
57.88 µs |
76.33 µs |
337.4 µs |
85.04 µs |
Zinc |
ScenarioAll/ParseAPI26 |
118.2 ns |
120.9 ns |
155.4 ns |
370.9 ns |
Zinc |
Throughput is the weakest category in the peer-only suite; see BENCKMARKS.md for the breakdown.
Latest local run of go test -count=1 ./... -coverprofile=coverage.out:
- Overall:
83.4% - Core (
github.com/0mjs/zinc):83.3% - Middleware (
github.com/0mjs/zinc/middleware):83.9%