gem

package module
v0.0.42 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 3, 2026 License: MIT Imports: 28 Imported by: 0

README

GemRouter

GemRouter is a fast and minimal HTTP router for Go, built on top of httprouter with a clean middleware chain, structured logging via log/slog, and zero-alloc context pooling.

Features

  • Fast radix tree routing via httprouter
  • Zero-alloc context pool (sync.Pool)
  • Structured logging with log/slog — plug in any logger (zap, zerolog, etc.)
  • Built-in middlewares: CORS, Recovery, Logger, Timeout, Prometheus
  • Route groups with per-group middleware
  • Graceful shutdown out of the box
  • Sonic JSON (3-5x faster than encoding/json)
  • JSON type alias for ergonomic responses

Installation

go get github.com/LynxBytes/GemRouter

Requires Go 1.22+

Quick start

package main

import gem "github.com/LynxBytes/GemRouter"

func main() {
    r := gem.DefaultGemRouter()

    r.GET("/ping", func(ctx *gem.GemContext) {
        ctx.ToJSON(200, gem.JSON{"message": "pong"})
    })

    if err := r.Run(); err != nil {
        log.Fatalf("failed to run server: %v", err)
    }
}

Routers

Constructor Middlewares CORS
BasicGemRouter() Nothing ✓ default
DefaultGemRouter() CORS, Recovery, Logger ✓ default
NewGemRouter(configs...) Recovery configurable
r := gem.NewGemRouter(
    gem.WithPort("3000"),
    gem.WithCorsDefault(),
    gem.WithJSONLogger(os.Stdout, slog.LevelInfo),
)

HTTP methods

r.GET("/users/:id", handler)
r.POST("/users", handler)
r.PUT("/users/:id", handler)
r.PATCH("/users/:id", handler)
r.DELETE("/users/:id", handler)

Parameters

// path param
r.GET("/users/:id", func(ctx *gem.GemContext) {
    id := ctx.Param("id")
    ctx.ToJSON(200, gem.JSON{"id": id})
})

// query param
r.GET("/search", func(ctx *gem.GemContext) {
    q := ctx.Query("q")
    ctx.String(200, q)
})

// wildcard
r.GET("/files/*path", handler)

JSON

// write
ctx.ToJSON(200, gem.JSON{"user": "mario", "age": 30})

// read
var body CreateUserRequest
if err := ctx.FromJSON(&body); err != nil {
    ctx.ToJSON(400, gem.JSON{"error": err.Error()})
    return
}

Validation

Built-in validator, no dependencies. Supports required, min=N, max=N, len=N, email.

r.POST("/users", func(ctx *gem.GemContext) {
    var body CreateUserRequest
    if err := ctx.FromJSON(&body); err != nil {
        ctx.ToJSON(400, gem.JSON{"error": err.Error()})
        return
    }

    v := gem.NewValidator().
        Check("name",  body.Name,  "required,min=2,max=50").
        Check("email", body.Email, "required,email").
        Check("age",   body.Age,   "min=18,max=120")

    if !v.Valid() {
        ctx.ToJSON(400, gem.JSON{"errors": v.Errors()})
        return
    }

    ctx.ToJSON(201, body)
})

Error response:

{
  "errors": [
    {"field": "email", "message": "must be a valid email"},
    {"field": "age",   "message": "must be at least 18"}
  ]
}
Rule Types Description
required any not empty or zero
min=N string, int, float64 min length / min value
max=N string, int, float64 max length / max value
len=N string exact length
email string valid email format

Response formatters

Configure a global shape for all success and error responses without touching each handler.

Success responses
r := gem.NewGemRouter(
    gem.WithResponseFormatter(func(code int, data any) (int, any) {
        return code, gem.JSON{
            "success": true,
            "data":    data,
        }
    }),
)

// in handlers
ctx.Success(200, user) // → {"success":true,"data":{...}}

Default (no formatter configured): ctx.Success(code, data) behaves identically to ctx.ToJSON(code, data).

Error responses

ctx.Fail is variadic — accepts one or more errors of any type.

r := gem.NewGemRouter(
    gem.WithErrorFormatter(func(code int, errs []any) (int, any) {
        return code, gem.JSON{
            "success": false,
            "errors":  errs,
            "code":    code,
        }
    }),
)

Default behavior without a formatter:

// single string → {"error":"..."}
ctx.Fail(400, "invalid email")

// multiple strings → {"errors":["...","..."]}
ctx.Fail(400, "name required", "email invalid")

// ValidationError slice → {"errors":[{"field":"...","message":"..."},...]}
ctx.Fail(422, v.Errors())
Both together (typical REST API)
r := gem.NewGemRouter(
    gem.WithResponseFormatter(func(code int, data any) (int, any) {
        return code, gem.JSON{"success": true, "data": data}
    }),
    gem.WithErrorFormatter(func(code int, errs []any) (int, any) {
        return code, gem.JSON{"success": false, "errors": errs}
    }),
)

r.POST("/users", func(ctx *gem.GemContext) {
    var body CreateUserRequest
    if err := ctx.FromJSON(&body); err != nil {
        ctx.Fail(400, "invalid body")
        return
    }

    v := gem.NewValidator().
        Check("name",  body.Name,  "required,min=2").
        Check("email", body.Email, "required,email")
    if !v.Valid() {
        ctx.Fail(422, v.Errors()) // → {"success":false,"errors":[...]}
        return
    }

    ctx.Success(201, body) // → {"success":true,"data":{...}}
})

The formatter can also override the status code by returning a different value as the first argument.

Middlewares

// global
r.Use(MyMiddleware)

// per route group
api := r.Group("/api", AuthMiddleware)
api.GET("/users", handler)

Writing a middleware:

func AuthMiddleware(next gem.GemHandler) gem.GemHandler {
    return func(ctx *gem.GemContext) {
        token := ctx.Header("Authorization")
        if !isValid(token) {
            ctx.ToJSON(401, gem.JSON{"error": "unauthorized"})
            return // chain stops here
        }
        next(ctx)
    }
}

Route groups

api := r.Group("/api")

v1 := api.Group("/v1", AuthMiddleware)
v1.GET("/users", getUsers)
v1.POST("/users", createUser)

v2 := api.Group("/v2", AuthMiddleware, RateLimitMiddleware)
v2.GET("/users", getUsersV2)

Built-in middlewares

// CORS
gem.WithCors(&gem.CorsConfig{
    AllowOrigins: []string{"https://example.com"},
    AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders: []string{"Content-Type", "Authorization"},
    AllowCredentials: true,
})

// Timeout
r.Use(gem.Timeout(5 * time.Second))

// Prometheus metrics
r := gem.NewGemRouter(
    gem.WithPrometheus("/metrics"),
)

Logging

Writer-based (stdout / any io.Writer)
// text
gem.WithTextLogger(os.Stdout, slog.LevelInfo)

// JSON
gem.WithJSONLogger(os.Stdout, slog.LevelInfo)

// custom slog logger
gem.WithLogger(mySlogLogger)
File
// text → file only
gem.WithTextFileLogger("logs/app.log", slog.LevelInfo)

// JSON → file only
gem.WithJSONFileLogger("logs/app.log", slog.LevelInfo)

// text → stdout + file
gem.WithTextTeeLogger("logs/app.log", slog.LevelInfo)

// JSON → stdout + file
gem.WithJSONTeeLogger("logs/app.log", slog.LevelInfo)

Files open in append mode. Invalid paths fall back to stdout automatically.

Log rotation

Uses lumberjack under the hood. No external config needed.

cfg := gem.LogRotateConfig{
    Path:       "logs/app.log",
    MaxSizeMB:  100,  // rotate after 100 MB
    MaxBackups: 5,    // keep 5 old files
    MaxAgeDays: 30,   // delete files older than 30 days
    Compress:   true, // gzip rotated files
}

// text → rotating file
gem.WithTextRotateLogger(cfg, slog.LevelInfo)

// JSON → rotating file
gem.WithJSONRotateLogger(cfg, slog.LevelInfo)

// text → stdout + rotating file
gem.WithTextTeeRotateLogger(cfg, slog.LevelInfo)

// JSON → stdout + rotating file
gem.WithJSONTeeRotateLogger(cfg, slog.LevelInfo)

The log file is closed automatically on graceful shutdown.

Split format (text console + JSON file)

When you want human-readable output in the terminal and structured JSON in the file at the same time:

// slog: text → stdout, JSON → file
gem.WithSplitLogger("logs/app.log", slog.LevelInfo)

// slog: text → stdout, JSON → rotating file
gem.WithSplitRotateLogger(gem.LogRotateConfig{
    Path:       "logs/app.log",
    MaxSizeMB:  100,
    MaxBackups: 5,
    Compress:   true,
}, slog.LevelInfo)

Each destination gets its own encoder — the same log record is written twice, independently.

Zap (optional)

Zap is not a required dependency. Import the zaplogger subpackage only if you need it.

go get github.com/LynxBytes/GemRouter/zaplogger
import (
    gem "github.com/LynxBytes/GemRouter"
    "github.com/LynxBytes/GemRouter/zaplogger"
    "go.uber.org/zap/zapcore"
)

// custom zap logger
r := gem.NewGemRouter(
    zaplogger.WithZapLogger(myZapLogger),
)

// zap → file only
zaplogger.WithZapFileLogger("logs/app.log", zapcore.InfoLevel)

// zap → stdout + file
zaplogger.WithZapTeeLogger("logs/app.log", zapcore.InfoLevel)

// zap → rotating file
zaplogger.WithZapRotateLogger(gem.LogRotateConfig{
    Path:       "logs/app.log",
    MaxSizeMB:  100,
    MaxBackups: 5,
    Compress:   true,
}, zapcore.InfoLevel)

// zap → stdout + rotating file
zaplogger.WithZapTeeRotateLogger(cfg, zapcore.InfoLevel)

// zap → stdout + custom writer
zaplogger.WithZapTeeWriter(myWriter, zapcore.InfoLevel)

// zap split: console encoder → stdout, JSON → file
zaplogger.WithZapSplitLogger("logs/app.log", zapcore.InfoLevel)

// zap split: console encoder → stdout, JSON → rotating file
zaplogger.WithZapSplitRotateLogger(gem.LogRotateConfig{
    Path:       "logs/app.log",
    MaxSizeMB:  100,
    MaxBackups: 5,
    Compress:   true,
}, zapcore.InfoLevel)
Inside handlers
ctx.Logger.Info("user created", slog.String("id", user.ID))

Context store

// set/get arbitrary values across middlewares
ctx.Set("userID", "123")
val, ok := ctx.Get("userID")

// typed fields
ctx.Store.RequestID
ctx.Store.UserID

Cookies

ctx.SetCookie("session", token, 3600, "/", "", true, true)
val, err := ctx.Cookie("session")
ctx.DeleteCookie("session")

Custom handlers

r := gem.NewGemRouter(
    gem.WithNotFound(func(ctx *gem.GemContext) {
        ctx.ToJSON(404, gem.JSON{"error": "not found"})
    }),
    gem.WithMethodNotAllowed(func(ctx *gem.GemContext) {
        ctx.ToJSON(405, gem.JSON{"error": "method not allowed"})
    }),
    gem.WithHealth(func(ctx *gem.GemContext) {
        ctx.ToJSON(200, gem.JSON{"status": "ok"})
    }),
)

Graceful shutdown

Built-in. Run() listens for SIGINT and SIGTERM and shuts down cleanly.

r := gem.NewGemRouter(
    gem.WithShutdownTimeout(10 * time.Second),
)
r.Run() // blocks until signal

Benchmarks

BenchmarkRouter_Ping-11          15937568     74 ns/op      16 B/op    1 allocs/op
BenchmarkRouter_Param-11         12995856     93 ns/op      48 B/op    2 allocs/op
BenchmarkRouter_ParallelPing-11  64808960     24 ns/op      16 B/op    1 allocs/op
BenchmarkRouter_NoContent-11     31490965     39 ns/op       0 B/op    0 allocs/op
BenchmarkRoutes_500-11           15444356     76 ns/op      32 B/op    1 allocs/op

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CorsConfig added in v0.0.5

type CorsConfig struct {
	AllowOrigins     []string
	AllowMethods     []string
	AllowHeaders     []string
	ExposeHeaders    []string
	AllowCredentials bool
	MaxAge           int
}

type ErrorFormatter added in v0.0.12

type ErrorFormatter func(code int, errs []any) (int, any)

type GemConfig added in v0.0.3

type GemConfig func(router *GemRouter)

func WithAddr added in v0.0.3

func WithAddr(addr string) GemConfig

func WithCors added in v0.0.5

func WithCors(cfg *CorsConfig) GemConfig

func WithCorsDefault added in v0.0.5

func WithCorsDefault() GemConfig

func WithErrorFormatter added in v0.0.12

func WithErrorFormatter(f ErrorFormatter) GemConfig

func WithHealth added in v0.0.3

func WithHealth(handler GemHandler) GemConfig

func WithIdleTimeout added in v0.0.7

func WithIdleTimeout(d time.Duration) GemConfig

func WithJSONFileLogger added in v0.0.12

func WithJSONFileLogger(path string, level slog.Level) GemConfig

func WithJSONLogger added in v0.0.8

func WithJSONLogger(w io.Writer, level slog.Level) GemConfig

func WithJSONRotateLogger added in v0.0.12

func WithJSONRotateLogger(cfg LogRotateConfig, level slog.Level) GemConfig

func WithJSONTeeLogger added in v0.0.12

func WithJSONTeeLogger(path string, level slog.Level) GemConfig

func WithJSONTeeRotateLogger added in v0.0.12

func WithJSONTeeRotateLogger(cfg LogRotateConfig, level slog.Level) GemConfig

func WithLogCloser added in v0.0.12

func WithLogCloser(c io.Closer) GemConfig

func WithLogger added in v0.0.5

func WithLogger(l *slog.Logger) GemConfig

func WithMethodNotAllowed added in v0.0.8

func WithMethodNotAllowed(handler GemHandler) GemConfig

func WithMiddleware added in v0.0.3

func WithMiddleware(middleware Middleware) GemConfig

func WithMiddlewares added in v0.0.3

func WithMiddlewares(middlewares []Middleware) GemConfig

func WithName added in v0.0.11

func WithName(name string) GemConfig

func WithPort added in v0.0.3

func WithPort(port string) GemConfig

func WithPrometheus added in v0.0.6

func WithPrometheus(metricsPath string) GemConfig

func WithReadTimeout added in v0.0.7

func WithReadTimeout(d time.Duration) GemConfig

func WithResponseFormatter added in v0.0.12

func WithResponseFormatter(f ResponseFormatter) GemConfig

func WithShutdownTimeout added in v0.0.5

func WithShutdownTimeout(d time.Duration) GemConfig

func WithSplitLogger added in v0.0.12

func WithSplitLogger(path string, level slog.Level) GemConfig

func WithSplitRotateLogger added in v0.0.12

func WithSplitRotateLogger(cfg LogRotateConfig, level slog.Level) GemConfig

func WithTextFileLogger added in v0.0.12

func WithTextFileLogger(path string, level slog.Level) GemConfig

func WithTextLogger added in v0.0.8

func WithTextLogger(w io.Writer, level slog.Level) GemConfig

func WithTextRotateLogger added in v0.0.12

func WithTextRotateLogger(cfg LogRotateConfig, level slog.Level) GemConfig

func WithTextTeeLogger added in v0.0.12

func WithTextTeeLogger(path string, level slog.Level) GemConfig

func WithTextTeeRotateLogger added in v0.0.12

func WithTextTeeRotateLogger(cfg LogRotateConfig, level slog.Level) GemConfig

func WithTrustedProxy added in v0.0.5

func WithTrustedProxy() GemConfig

func WithVersion added in v0.0.11

func WithVersion(version string) GemConfig

func WithWriteTimeout added in v0.0.7

func WithWriteTimeout(d time.Duration) GemConfig

type GemContext

type GemContext struct {
	Writer  http.ResponseWriter
	Request *http.Request
	Logger  *slog.Logger
	Pattern string
	// contains filtered or unexported fields
}

func NewTestContext added in v0.0.15

func NewTestContext(w http.ResponseWriter, r *http.Request) *GemContext

func (*GemContext) Context added in v0.0.41

func (context *GemContext) Context() context.Context

func (*GemContext) Cookie added in v0.0.5

func (context *GemContext) Cookie(name string) (string, error)

func (*GemContext) Copy added in v0.0.5

func (context *GemContext) Copy() *GemContext

func (*GemContext) DeleteCookie added in v0.0.5

func (context *GemContext) DeleteCookie(name string)

func (*GemContext) Fail added in v0.0.12

func (context *GemContext) Fail(code int, errs ...any)

func (*GemContext) FromJSON added in v0.0.5

func (context *GemContext) FromJSON(data any) error

func (*GemContext) Get

func (context *GemContext) Get(key string) string

func (*GemContext) GetClientIP added in v0.0.40

func (context *GemContext) GetClientIP() string

func (*GemContext) GetUserAgent added in v0.0.40

func (context *GemContext) GetUserAgent() string

func (*GemContext) Header

func (context *GemContext) Header(key string) string

func (*GemContext) Method

func (context *GemContext) Method() string

func (*GemContext) NOTFOUND added in v0.0.3

func (context *GemContext) NOTFOUND()

func (*GemContext) NoContent

func (context *GemContext) NoContent(code int)

func (*GemContext) OK

func (context *GemContext) OK()

func (*GemContext) Param

func (context *GemContext) Param(key string) string

func (*GemContext) Path

func (context *GemContext) Path() string

func (*GemContext) Query

func (context *GemContext) Query(key string) string

func (*GemContext) RequestID added in v0.0.5

func (context *GemContext) RequestID() string

func (*GemContext) Set

func (context *GemContext) Set(key, value string)

func (*GemContext) SetCookie added in v0.0.5

func (context *GemContext) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)

func (*GemContext) SetParam added in v0.0.19

func (context *GemContext) SetParam(key, value string)

func (*GemContext) Status

func (context *GemContext) Status(code int)

func (*GemContext) StatusCode

func (context *GemContext) StatusCode() int

func (*GemContext) String

func (context *GemContext) String(code int, text string)

func (*GemContext) Success added in v0.0.12

func (context *GemContext) Success(code int, data any)

func (*GemContext) ToJSON

func (context *GemContext) ToJSON(code int, data any)

type GemGroup

type GemGroup struct {
	// contains filtered or unexported fields
}

func (*GemGroup) DELETE

func (g *GemGroup) DELETE(pattern string, handler GemHandler)

func (*GemGroup) GET

func (g *GemGroup) GET(pattern string, handler GemHandler)

func (*GemGroup) Group added in v0.0.5

func (g *GemGroup) Group(prefix string, middlewares ...Middleware) *GemGroup

func (*GemGroup) PATCH

func (g *GemGroup) PATCH(pattern string, handler GemHandler)

func (*GemGroup) POST

func (g *GemGroup) POST(pattern string, handler GemHandler)

func (*GemGroup) PUT

func (g *GemGroup) PUT(pattern string, handler GemHandler)

func (*GemGroup) Use added in v0.0.5

func (g *GemGroup) Use(m Middleware)

type GemHandler

type GemHandler func(ctx *GemContext)

func Logger

func Logger(next GemHandler) GemHandler

func Recovery

func Recovery(next GemHandler) GemHandler

type GemRouter

type GemRouter struct {
	Addr string
	Port string

	Health GemHandler
	// contains filtered or unexported fields
}

func BasicGemRouter added in v0.0.6

func BasicGemRouter() *GemRouter

func DefaultGemRouter added in v0.0.5

func DefaultGemRouter() *GemRouter

func NewGemRouter

func NewGemRouter(configs ...GemConfig) *GemRouter

func (*GemRouter) ConsoleWriter added in v0.0.14

func (r *GemRouter) ConsoleWriter() io.Writer

func (*GemRouter) DELETE

func (r *GemRouter) DELETE(pattern string, handler GemHandler)

func (*GemRouter) GET

func (r *GemRouter) GET(pattern string, handler GemHandler)

func (*GemRouter) Group

func (r *GemRouter) Group(prefix string, middlewares ...Middleware) *GemGroup

func (*GemRouter) HandleSystemErrors added in v0.0.17

func (r *GemRouter) HandleSystemErrors()

func (*GemRouter) PATCH

func (r *GemRouter) PATCH(pattern string, handler GemHandler)

func (*GemRouter) POST

func (r *GemRouter) POST(pattern string, handler GemHandler)

func (*GemRouter) PUT

func (r *GemRouter) PUT(pattern string, handler GemHandler)

func (*GemRouter) Run

func (r *GemRouter) Run() error

func (*GemRouter) ServeHTTP added in v0.0.15

func (r *GemRouter) ServeHTTP(w http.ResponseWriter, req *http.Request)

func (*GemRouter) Use

func (r *GemRouter) Use(middleware Middleware)

type JSON added in v0.0.8

type JSON = map[string]any

type LogRotateConfig added in v0.0.12

type LogRotateConfig struct {
	Path       string
	MaxSizeMB  int
	MaxBackups int
	MaxAgeDays int
	Compress   bool
}

type Middleware

type Middleware func(GemHandler) GemHandler

func Cors added in v0.0.5

func Cors(cfg *CorsConfig) Middleware

func Timeout added in v0.0.5

func Timeout(d time.Duration) Middleware

type ResponseFormatter added in v0.0.12

type ResponseFormatter func(code int, data any) (int, any)

type Rule added in v0.0.22

type Rule func(value any) *ValidationError

func And added in v0.0.31

func And(rules ...Rule) Rule

func Email added in v0.0.22

func Email(ev validators.EmailChecker) Rule

func Empty added in v0.0.28

func Empty() Rule

func Enum added in v0.0.22

func Enum(valid func() bool, message string) Rule

func If added in v0.0.25

func If(do bool, rules ...Rule) Rule

func Len added in v0.0.22

func Len(n int) Rule

func Max added in v0.0.22

func Max(n int) Rule

func Min added in v0.0.22

func Min(n int) Rule

func NotEmpty added in v0.0.31

func NotEmpty() Rule

func NotNull added in v0.0.31

func NotNull() Rule

func Null added in v0.0.28

func Null() Rule

func Or added in v0.0.29

func Or(rules ...Rule) Rule

func Required added in v0.0.22

func Required() Rule

type ValidationError added in v0.0.8

type ValidationError struct {
	Field   string `json:"field"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

type Validator added in v0.0.23

type Validator struct {
	EmailValidator validators.EmailChecker
	// contains filtered or unexported fields
}

func NewValidator added in v0.0.8

func NewValidator() *Validator

func (*Validator) Check added in v0.0.23

func (v *Validator) Check(field string, value any, rules ...Rule) *Validator

func (*Validator) Errors added in v0.0.23

func (v *Validator) Errors() []ValidationError

func (*Validator) SetEmailValidator added in v0.0.24

func (v *Validator) SetEmailValidator(ev validators.EmailChecker) *Validator

func (*Validator) Valid added in v0.0.23

func (v *Validator) Valid() bool

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL