Zinc
Galvanize net/http.
The high-performance, minimalist web layer for Go. Routing, binding, errors, and 27 middleware packages, applied as a thin coat over the standard library you already trust.
app := zinc.New() app.Get("/users/{id}", func(c *zinc.Context) error { user, ok := users[c.Param("id")] if !ok { return zinc.NotFound("user not found") } return c.JSON(user)}) app.Listen(":8080")$ curl -i localhost:8080/users/42
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":"42","name":"Ada Lovelace"}
What you get
One request, start to finish.
From the first middleware to the response, each step is a few lines of Go.
app.Use(
requestid.New(),
logger.New(),
recover.New(),
secure.New(),
)
api := app.Group("/api", timeout.New(timeout.Config{Timeout: 5 * time.Second}))L2Every response carries an ID that your logs share.Guide →
GET /api/users/42
200 OK
api := app.Group("/api", requireAuth)
api.Get("/users/{id}", func(c *zinc.Context) error {
return c.JSON(zinc.Map{"id": c.Param("id")})
})
app.RouteNotFound("/api/{tail...}", apiNotFound)L4{id} is captured from the path and read with c.Param.Guide →
GET /api/users/42
200 OK
type CreateUser struct {
Name string `json:"name"`
Email string `json:"email"`
}
app.Post("/users", func(c *zinc.Context) error {
var in CreateUser
if err := c.Bind().JSON(&in); err != nil {
return err // 400, naming the field
}
return c.Status(zinc.StatusCreated).JSON(in)
})L8Decodes the body into CreateUser; bad input becomes a 400.Guide →
POST /users {"name":"Ada","email":"ada@example.com"}
201 Created
app.Get("/users/{id}", func(c *zinc.Context) error {
user, err := users.Find(c.Param("id"))
if err != nil {
return zinc.NotFound("user not found")
}
return c.JSON(user)
})L4Return an error; the error handler writes the response.Guide →
GET /users/nope
404 Not Found
OpenAPI · built in
Your types are the spec.
Every app serves an OpenAPI 3.1 spec at /openapi.json and a reference page at /docs, built from the handlers you already wrote. A rule shows up in the spec only when Zinc checks it on requests.
type CreateUser struct {
OrgID string `path:"org"`
DryRun bool `query:"dry_run"`
Email string `json:"email" validate:"required,email"`
}
api.Post("/orgs/{org}/users", zinc.Typed(createUser)).
Status(http.StatusCreated)In the specorg · in: path · string · required
POST /api/orgs/acme/users {"email":"ada@example.com"}
201 {"id":1,"org_id":"acme","email":"ada@example.com"}
type CreateUser struct {
OrgID string `path:"org"`
DryRun bool `query:"dry_run"`
Email string `json:"email" validate:"required,email"`
}
api.Post("/orgs/{org}/users", zinc.Typed(createUser)).
Status(http.StatusCreated)In the specdry_run · in: query · boolean
POST /api/orgs/acme/users?dry_run=yes
400 "dry_run": "must be a boolean"
type CreateUser struct {
OrgID string `path:"org"`
DryRun bool `query:"dry_run"`
Email string `json:"email" validate:"required,email"`
}
api.Post("/orgs/{org}/users", zinc.Typed(createUser)).
Status(http.StatusCreated)In the specrequired: ["email"] · email: format: "email"
{}
422 "email": "is required"
{"email":"nope"}
422 "email": "must be an email address"
type CreateUser struct {
OrgID string `path:"org"`
DryRun bool `query:"dry_run"`
Email string `json:"email" validate:"required,email"`
}
api.Post("/orgs/{org}/users", zinc.Typed(createUser)).
Status(http.StatusCreated)In the spec201 → User · 400, 422, 500 → Error
{"email":
400 "invalid request body"
Pick one to see it in the spec and on the wire.
- Path parametersA path tag is a required parameter, typed from its field.
- Query parametersParsed into the field's type; a value that won't parse is a 400 naming it.
- Validationvalidate tags become schema keywords, and every request is checked against them.
- Responses and errorsThe return type is the 201 body; errors take your error handler's shape.
How it fits
A thin coat, not a new engine.
Galvanizing protects steel without replacing it. Zinc does the same for net/http: your app is still an http.Handler, and standard handlers, middleware, and servers plug straight in.
Pick one to see where it plugs into the stack.
- 1Run it on a server you configure
server.Handler = app - 2Serve a route with any standard handler
app.HandleHTTP("GET /metrics", promhttp.Handler()) - 3Wrap the app in standard middleware
app.UseHTTP(otelhttp.NewMiddleware("api"))
zinc.HandlerFunchttp.Handler
Measured, in the repo
61/ 93
Scenarios where Zinc recorded the lowest latency against Gin and Echo. On average it was 6.1% from the fastest. Gin led 21 and Echo 2. Against the bare routers BunRouter and Chi, Zinc led 32 of 66. All were measured together in one run for 0.7.3.
Zinc fastestA tie within 3%A rival fastest
5 October 2026Apple M1 Pro · go1.27.1 · commit 2a51380
Full benchmark report →HelloWorldns/op · lower is faster
Middleware
A periodic table of production parts.
Numbered in chain order: each part wraps everything after it. Select parts to galvanize them into a stack, and the table writes the app.Use call in the right order.
Your compound
RiLgRcSh
app.Use(
requestid.New(),
logger.New(),
recover.New(),
secure.New(),
)Coat your first service.
Start a new API in a few minutes, or add Zinc to a service you already run.