gem

package module
v0.0.12 Latest Latest
Warning

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

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 23 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 ContextStore added in v0.0.6

type ContextStore struct {
	RequestID string
	UserID    string
	// contains filtered or unexported fields
}

func (*ContextStore) Get added in v0.0.6

func (store *ContextStore) Get(key string) (any, bool)

func (*ContextStore) Set added in v0.0.6

func (store *ContextStore) Set(key string, val any)

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)

ErrorFormatter transforms an error response before writing. errs contains one or more errors: strings, ValidationError slices, or any custom type. Returns the final status code and body to serialize.

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 WithNotFound added in v0.0.3

func WithNotFound(handler GemHandler) 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
	Store   *ContextStore
	Logger  *slog.Logger
	Pattern string
	// contains filtered or unexported fields
}

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)

Fail writes a formatted error response using the configured ErrorFormatter. Accepts one or more errors: strings, []ValidationError, or any custom type.

func (*GemContext) FromJSON added in v0.0.5

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

func (*GemContext) Get

func (context *GemContext) Get(key string) (any, bool)

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 string, val any)

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

Success writes a formatted success response using the configured ResponseFormatter.

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

	NotFound         GemHandler
	MethodNotAllowed GemHandler
	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) 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) NoRoute

func (r *GemRouter) NoRoute(handler GemHandler)

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) Use

func (r *GemRouter) Use(middleware Middleware)

type GemValidator added in v0.0.8

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

func NewValidator added in v0.0.8

func NewValidator() *GemValidator

func (*GemValidator) Check added in v0.0.8

func (v *GemValidator) Check(field string, value any, rules string) *GemValidator

func (*GemValidator) Errors added in v0.0.8

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

func (*GemValidator) Valid added in v0.0.8

func (v *GemValidator) Valid() bool

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)

ResponseFormatter transforms a success response before writing. Returns the final status code and body to serialize.

type ValidationError added in v0.0.8

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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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