Skip to content

0mjs/zinc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

89 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Version Go Version Docs Coverage Go Report Card License

Zinc

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.

Features

  • Express-style routes with :param and *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/template and text/template
  • Static/file serving and stdlib interop via Mount, Wrap, and WrapFunc
  • Explicit startup and shutdown with Listen, Serve, and Shutdown
  • First-party middleware and a small in-memory jobs add-on in one module

Installation

go get github.com/0mjs/zinc

Requires Go 1.25 or newer.

Quick Start

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()
}

Routing and Middleware

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.

Binding and Responses

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.

Configuration

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"})
})

Middleware

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.

Background Jobs

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.

Benchmarks

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.

Quality

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%

License

MIT

About

💫 Express-inspired Go API framework built on net/http

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors