shinobi

package module
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2026 License: BSD-3-Clause Imports: 23 Imported by: 0

README

Shinobi

Shinobi is a lightweight HTTP micro-framework for Go 1.22+, built on top of the standard library. It wraps net/http with a rich request context, expressive routing, and a clean middleware API — without sacrificing compatibility with the Go ecosystem.

Go Version Go Reference Go Report Card CI codecov License

v0.x — Shinobi is in active development. The API is stabilizing but breaking changes may still occur between minor versions. It can be used in production — pin your version in go.mod and review the CHANGELOG before upgrading.

Features

  • Rich Context (Ctx) — Unified API for request, response, params, cookies, rendering, and more.
  • Error-returning Handlers — Handlers return error, enabling clean centralized error handling.
  • Expressive Routing — Groups, multi-method routes, prefix mounting, and route introspection.
  • Middleware — Global, group-level, or per-route middleware with Use, With, and With().Route().
  • Rate Limiting — Per-IP sliding window rate limiter with configurable limit and window.
  • Real IP — Extract the true client IP behind reverse proxies with optional trusted CIDR enforcement.
  • WebSocket — Adapter interface to integrate any WebSocket library (gorilla, nhooyr, …) without coupling to a specific implementation.
  • stdlib Adapter — Bridge http.Handler and func(http.Handler) http.Handler into shinobi with AdaptHTTP and Adapt.
  • Static File Serving — Serve local directories or embedded filesystems with FileServer and FileServerFS.
  • Built-in Rendering — JSON, XML, plain text, HTML templates, binary blobs, and file serving.
  • Server LifecycleListen, ListenTLS, ListenGraceful, ListenTLSGraceful, and a custom Server() builder.
  • Functional Options — Clean configuration via WithPrefix, WithRenderer, WithErrorHandler, and more.

Installation

go get github.com/nanoninja/shinobi

Quick Start

package main

import (
    "log"
    "log/slog"
    "net/http"
    "os"
    "time"

    "github.com/nanoninja/shinobi"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    app := shinobi.New(
        shinobi.WithPrefix("/api"),
        shinobi.WithLogger(logger),
    )

    app.Get("/health", func(c shinobi.Ctx) error {
        return c.String(http.StatusOK, "OK")
    })

    app.Group("/v1", func(r shinobi.Router) {
        r.Get("/users/{id}", getUser)
        r.Post("/users", createUser)
    })

    log.Fatal(app.ListenGraceful(":8080", 10*time.Second))
}

Configuration

Shinobi uses the Functional Options pattern. All options configure the underlying router.

app := shinobi.New(
    shinobi.WithPrefix("/api/v1"),
    shinobi.WithLogger(logger),
    shinobi.WithDebug(true),
    shinobi.WithRenderer(tmplRenderer),
    shinobi.WithBinder(shinobi.JSONBinder(shinobi.JSONStrict())),
    shinobi.WithValidator(myValidator),
    shinobi.WithErrorHandler(myErrorHandler),
    shinobi.WithNotFound(notFoundHandler),
    shinobi.WithMux(http.NewServeMux()),
)
Option Description
WithPrefix(string) Sets a global base path for all routes.
WithLogger(*slog.Logger) Sets the logger injected into each Ctx. Defaults to slog.Default().
WithDebug(bool) Enables debug mode — error responses include the original error message.
WithRenderer(render.Renderer) Sets the template renderer injected into each Ctx.
WithBinder(Binder) Sets the binder used to decode request bodies in Ctx.Bind.
WithValidator(Validator) Sets the validator used in Ctx.Validate.
WithErrorHandler(ErrorHandler) Overrides the default 500 error handler.
WithNotFound(HandlerFunc) Sets a custom 404 handler.
WithMux(*http.ServeMux) Injects a custom ServeMux instance.

Size Constants

Shinobi exports KB, MB, and GB constants for use with any byte-size parameter (MaxSize, BodyLimit, JSONMaxBytes, …):

shinobi.KB // 1 024 bytes
shinobi.MB // 1 048 576 bytes
shinobi.GB // 1 073 741 824 bytes

// Examples
shinobi.NewImageUpload(f, h, 5*shinobi.MB)
app.Use(middleware.BodyLimit(10 * shinobi.MB))
shinobi.JSONMaxBytes(shinobi.MB)

Routing

HTTP Verb Shortcuts
app.Get("/users", listUsers)
app.Post("/users", createUser)
app.Put("/users/{id}", updateUser)
app.Patch("/users/{id}", patchUser)
app.Delete("/users/{id}", deleteUser)
Path Parameters
app.Get("/users/{id}", func(c shinobi.Ctx) error {
    id := c.Param("id")
    return c.String(http.StatusOK, "User: %s", id)
})
Multi-Method Routes

Register multiple methods on a single path without repetition:

app.Route("/users/{id}", func(rt shinobi.Route) {
    rt.Get(getUser)
    rt.Put(updateUser)
    rt.Delete(deleteUser)
})

To apply a middleware to all methods of a route, use With before Route:

app.With(authMiddleware).Route("/users/{id}", func(rt shinobi.Route) {
    rt.Get(getUser)
    rt.Put(updateUser)
    rt.Delete(deleteUser)
})
Groups

Organize routes under a shared prefix. Each group inherits the parent middleware stack independently:

app.Group("/api", func(r shinobi.Router) {
    r.Use(authMiddleware)

    r.Group("/v1", func(v1 shinobi.Router) {
        v1.Get("/users", listUsers)
        v1.Post("/users", createUser)
    })
})
Mount

Attach a shinobi Handler at a given prefix. Use AdaptHTTP to mount stdlib handlers:

// Mount a shinobi handler
app.Mount("/admin", adminHandler)

// Mount a stdlib http.Handler (third-party router, custom handler...)
app.Mount("/metrics", shinobi.AdaptHTTP(promhttp.Handler()))
Static Files

FileServer and FileServerFS return a shinobi Handler ready to use with Mount:

// Serve from a local directory
app.Mount("/assets", shinobi.FileServer("./public"))

// Serve from an embedded filesystem (Go embed)
//go:embed public
var embedded embed.FS

app.Mount("/assets", shinobi.FileServerFS(embedded))

// Use fs.Sub to rebase the root — files at public/style.css are served as /assets/style.css
sub, _ := fs.Sub(embedded, "public")
app.Mount("/assets", shinobi.FileServerFS(sub))
Route Introspection
for _, route := range app.Routes() {
    fmt.Printf("%s %s\n", route.Method, route.Pattern)
}

Handler

Shinobi handlers return an error, enabling centralized error handling at the framework level:

type Handler interface {
    Handle(c Ctx) error
}

// Use a plain function with HandlerFunc
app.Get("/ping", shinobi.HandlerFunc(func(c shinobi.Ctx) error {
    return c.String(http.StatusOK, "pong")
}))

Context (Ctx)

Every handler receives a Ctx that wraps the HTTP request and response.

Request
c.Request()                  // *http.Request
c.Method()                   // "GET", "POST", ...
c.Path()                     // "/users/42"
c.Host()                     // "example.com"
c.Scheme()                   // "https" or "http"
c.Param("id")                // path parameter
c.Query("page")              // query string value
c.QueryValues()              // url.Values
c.FormValue("name")          // form field
c.FormFile("avatar")         // multipart file upload
c.ContentType()              // "application/json"
c.UserAgent()                // User-Agent header
c.Referer()                  // Referer header
c.IsMethod("POST")           // case-insensitive method check
c.IsSecure()                 // HTTPS check
c.IsXHR()                    // XMLHttpRequest check
c.IsWebSocket()              // WebSocket upgrade check
Bind

Bind decodes the request body into a struct based on the Content-Type header. JSON and XML are supported out of the box via DefaultBinder.

app.Post("/users", func(c shinobi.Ctx) error {
    var u CreateUser
    if err := c.Bind(&u); err != nil {
        return err
    }
    return c.JSON(http.StatusCreated, u)
})

Use WithBinder to customize the binder:

app := shinobi.New(
    shinobi.WithBinder(shinobi.BinderRegistry{
        "application/json": shinobi.JSONBinder(
            shinobi.JSONStrict(),
            shinobi.JSONMaxBytes(shinobi.MB),
        ),
        "application/xml": shinobi.XMLBinder(),
    }),
)
BindQuery

BindQuery decodes URL query parameters into a struct using query:"" field tags. Pointer fields are optional — nil when the parameter is absent.

type SearchParams struct {
    Query     string    `query:"q"`
    Page      int       `query:"page"`
    Active    bool      `query:"active"`
    CreatedAt time.Time `query:"created_at"`                      // RFC3339, "2006-01-02 15:04:05" or "2006-01-02"
    BirthDate time.Time `query:"birth_date" format:"2006-01-02"`  // custom format
}

app.Get("/search", func(c shinobi.Ctx) error {
    var p SearchParams
    if err := c.BindQuery(&p); err != nil {
        return err
    }
    return c.JSON(http.StatusOK, p)
})

Supported types: string, int, int64, float64, bool, time.Time, and their pointer variants.

Use format:"" to specify a custom Go time layout. When absent, the following formats are tried in order: time.RFC3339, time.DateTime, time.DateOnly.

BindForm

BindForm decodes application/x-www-form-urlencoded fields into a struct using form:"" field tags. Same rules as BindQuery — pointer fields are optional, time.Time and format:"" are supported.

type RegisterForm struct {
    Username  string    `form:"username"`
    BirthDate time.Time `form:"birth_date" format:"2006-01-02"`
}

app.Post("/register", func(c shinobi.Ctx) error {
    var f RegisterForm
    if err := c.BindForm(&f); err != nil {
        return err
    }
    return c.JSON(http.StatusOK, f)
})

BindForm is also available via c.Bind when the Content-Type is application/x-www-form-urlencoded.

Extending with a custom Binder

BinderRegistry is open for extension — add any MIME type without modifying Shinobi.

The built-in FormBinder supports string, int, int64, float64, bool, and time.Time. For more advanced use cases — nested structs, slices, custom types — you can replace it with a third-party decoder such as gorilla/schema:

import "github.com/gorilla/schema"

type schemaFormBinder struct {
    decoder *schema.Decoder
}

func (b *schemaFormBinder) Bind(r *http.Request, v any) error {
    if err := r.ParseForm(); err != nil {
        return err
    }
    return b.decoder.Decode(v, r.PostForm)
}

app := shinobi.New(
    shinobi.WithBinder(shinobi.BinderRegistry{
        "application/json":                  shinobi.JSONBinder(shinobi.JSONStrict()),
        "application/xml":                   shinobi.XMLBinder(),
        "application/x-www-form-urlencoded": &schemaFormBinder{decoder: schema.NewDecoder()},
    }),
)
Validate

Validate validates a value using the configured Validator. No default implementation is provided — plug in the library of your choice via WithValidator. Calling c.Validate without a configured validator returns ErrValidatorNotSet.

Implement the Validator interface:

type Validator interface {
    Validate(v any) error
}

Example with go-playground/validator:

import "github.com/go-playground/validator/v10"

type customValidator struct {
    v *validator.Validate
}

func (cv *customValidator) Validate(v any) error {
    return cv.v.Struct(v)
}

app := shinobi.New(
    shinobi.WithValidator(&customValidator{
        v: validator.New(),
    }),
)

app.Post("/users", func(c shinobi.Ctx) error {
    var u CreateUser
    if err := c.Bind(&u); err != nil {
        return err
    }
    if err := c.Validate(&u); err != nil {
        return err
    }
    return c.JSON(http.StatusCreated, u)
})
Self-validating types

Any type that implements the Validatable interface is validated directly by c.Validate, bypassing the globally configured Validator. This is useful for types that carry their own validation logic.

type Validatable interface {
    Validate() error
}
File Upload Validation

FileUpload implements Validatable and validates a multipart file against size, MIME type, and extension constraints. Pass it to c.Validate after calling c.FormFile.

Three constructors are available depending on the use case:

// Preset for web images (JPEG, PNG, GIF, WebP)
upload := shinobi.NewImageUpload(f, header, 2*shinobi.MB)

// Preset for PDF documents
upload := shinobi.NewDocumentUpload(f, header, 10*shinobi.MB)

// Bare constructor — configure constraints manually
upload := shinobi.NewFileUploadValidator(f, header)
upload.MaxSize = 2 << 20
upload.AllowedTypes = []string{"image/jpeg", "image/png"}
upload.AllowedExtensions = []string{".jpg", ".png"}

Use AddType and AddExtension to extend a preset without replacing it:

app.Post("/upload", func(c shinobi.Ctx) error {
    f, header, err := c.FormFile("avatar")
    if err != nil {
        return shinobi.HTTPError(http.StatusBadRequest, err.Error())
    }
    defer f.Close()

    upload := shinobi.NewImageUpload(f, header, 2*shinobi.MB).
        AddType("image/webp").
        AddExtension(".webp")
    if err := c.Validate(upload); err != nil {
        return err
    }
    // f is ready to read — Seek(0) was called after each check
    return c.NoContent()
})

Size is measured by reading the actual file body, not from the client-declared Header.Size. For a hard cap at the transport level, combine with middleware.BodyLimit:

app.Use(middleware.BodyLimit(10 << 20)) // 10 MB max across all routes

MIME type is detected from the first 512 bytes of the file content (http.DetectContentType), not from the Content-Type header sent by the client. This covers common formats (images, PDF, ZIP…) but not all — SVG is detected as text/xml. Provide a custom DetectFunc to use a more complete library:

import "github.com/gabriel-vasile/mimetype"

upload := shinobi.NewImageUpload(f, header, 2*shinobi.MB).AddType("image/svg+xml")
upload.DetectFunc = func(b []byte) string { return mimetype.Detect(b).String() }

Extension is checked against the filename declared by the client. It is spoofable on its own — always combine with AllowedTypes for reliable validation.

Context Values

Values are stored in the request's context chain (context.WithValue under the hood):

c.Set("user", user)          // store
v, ok := c.Get("user")       // retrieve

// Stdlib context access
c.Context()                  // context.Context from the request
c.WithContext(newCtx)        // returns a new Ctx with updated context
Response
c.Response()                 // *Response (wrapped ResponseWriter)
c.SetHeader("X-Custom", "v")
c.AddHeader("Vary", "Accept")
Rendering
c.JSON(http.StatusOK, data)
c.XML(http.StatusOK, data)
c.CSV(http.StatusOK, [][]string{{"name", "age"}, {"alice", "30"}})
c.String(http.StatusOK, "Hello, %s", name)
c.HTML(http.StatusOK, "index.html", data)   // requires WithRenderer
c.Blob(http.StatusOK, pdfBytes, render.MimePDF())
c.NoContent()
Files
c.File("/path/to/file.pdf")
c.Attachment("/path/to/report.pdf")   // Content-Disposition: attachment
c.Inline("/path/to/preview.pdf")      // Content-Disposition: inline
Navigation
return c.Redirect("/login", http.StatusFound)
Cookies
c.SetCookie(&http.Cookie{Name: "session", Value: "abc"})
c.DeleteCookie("session", "/")

Middleware

Middlewares wrap a Handler to extend or intercept request processing:

type Middleware func(Handler) Handler
Applying Middleware
// Global
app.Use(loggerMiddleware, authMiddleware)

// Group-level
app.Group("/admin", func(r shinobi.Router) {
    r.Use(requireAdmin)
    r.Get("/dashboard", dashboard)
})

// Per-route scoping
app.With(rateLimitMiddleware).Post("/login", handleLogin)
Writing a Middleware
func Logger(next shinobi.Handler) shinobi.Handler {
    return shinobi.HandlerFunc(func(c shinobi.Ctx) error {
        start := time.Now()
        err := next.Handle(c)
        slog.Info("request",
            "method", c.Method(),
            "path", c.Path(),
            "duration", time.Since(start),
        )
        return err
    })
}
Adapting stdlib Middleware
// func(http.Handler) http.Handler → shinobi Middleware
app.Use(shinobi.Adapt(corsMiddleware))
app.Use(shinobi.Adapt(tracingMiddleware))

// http.Handler → shinobi Handler
app.Mount("/metrics", shinobi.AdaptHTTP(promhttp.Handler()))

Adapt propagates any context changes made by the stdlib middleware (e.g. r.WithContext(...)) back into the shinobi Ctx.

Built-in Middlewares

Shinobi provides built-in middlewares in the middleware sub-package.

RequestID

RequestID assigns a unique ID to each request via the X-Request-ID header. If the incoming request already carries the header, it is reused — useful for distributed tracing. The ID is also stored in the request context.

app.Use(middleware.RequestID())

// retrieve in a handler
app.Get("/", func(c shinobi.Ctx) error {
    id, _ := c.Get(middleware.RequestIDKey)
    return c.String(http.StatusOK, "request id: %s", id)
})
Logger

Logger logs each request using slog. It records the HTTP method, path, status code, duration, and request ID if present.

app.Use(middleware.RequestID())
app.Use(middleware.Logger())

Example output:

{"time":"2026-04-19T10:00:00Z","level":"INFO","msg":"request","method":"GET","path":"/users","status":200,"duration":"1.2ms","request_id":"a1b2c3d4-..."}
Recoverer

Recoverer catches panics in handlers, logs the error and stack trace via slog, and returns a 500 Internal Server Error through the configured error handler.

import "github.com/nanoninja/shinobi/middleware"

app.Use(middleware.Recoverer())

The panic is wrapped in a *StatusError with the original cause available for logging:

app := shinobi.New(
    shinobi.WithErrorHandler(func(err error, c shinobi.Ctx) {
        if e, ok := err.(*shinobi.StatusError); ok {
            if e.Cause != nil {
                slog.Error("panic recovered", "cause", e.Cause)
            }
            _ = c.JSON(e.Code, map[string]any{"error": "internal server error"})
            return
        }
        _ = c.JSON(http.StatusInternalServerError, map[string]any{"error": err.Error()})
    }),
)
app.Use(middleware.Recoverer())
Compress

Compress compresses the response body using gzip or deflate, selected from the client's Accept-Encoding header. It sets Content-Encoding and Vary: Accept-Encoding headers and removes Content-Length.

import (
    "compress/gzip"
    "github.com/nanoninja/shinobi/middleware"
)

app.Use(middleware.Compress(gzip.DefaultCompression))

The level parameter controls compression strength. Use constants from compress/gzip or compress/flate (e.g. gzip.BestSpeed, flate.BestCompression). An invalid level falls back to no compression.

Timeout

Timeout cancels the request context after the given duration. If the deadline is exceeded before the handler writes a response, a 503 Service Unavailable is returned through the error handler.

app.Use(middleware.Timeout(5 * time.Second))

Note: Timeout only works if the handler respects context cancellation — for example by passing the context to database queries or outgoing HTTP calls. CPU-bound work that never checks ctx.Err() will not be interrupted.

CORS

CORS adds Cross-Origin Resource Sharing headers to every response. Preflight requests (OPTIONS) are handled automatically and short-circuited with a 204 No Content.

// Permissive defaults — suitable for development
app.Use(middleware.CORS(middleware.DefaultCORSConfig()))

// Restrict to specific origins
app.Use(middleware.CORS(middleware.DefaultCORSConfig("https://myapp.com", "https://staging.myapp.com")))

// Fully custom
app.Use(middleware.CORS(middleware.CORSConfig{
    AllowedOrigins:   []string{"https://myapp.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
    AllowedHeaders:   []string{"Content-Type", "Authorization"},
    AllowCredentials: true,
    MaxAge:           3600,
}))

When multiple specific origins are provided, the middleware reflects only the matched request Origin in the response and adds Vary: Origin automatically.

Note: AllowCredentials: true is incompatible with a wildcard origin (*). Browsers will reject such a combination — always specify explicit origins when credentials are needed.

SecureHeaders

SecureHeaders sets common security-related HTTP response headers with safe, opinionated defaults.

app.Use(middleware.SecureHeaders())
Header Value
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection 0
Referrer-Policy strict-origin-when-cross-origin

SecureHeadersWithHSTS adds Strict-Transport-Security on top of the standard headers. Only use it in production behind HTTPS — enabling HSTS on a plain HTTP server makes the site unreachable over HTTP for the duration of maxAge.

// 1 year, current domain only
app.Use(middleware.SecureHeadersWithHSTS(31536000, false))

// 1 year, current domain + all subdomains
app.Use(middleware.SecureHeadersWithHSTS(31536000, true))
BodyLimit

BodyLimit caps the request body size. Requests exceeding the limit receive a 413 Request Entity Too Large through the error handler.

app.Use(middleware.BodyLimit(shinobi.MB))
BasicAuth

BasicAuth enforces HTTP Basic Authentication. Unauthenticated requests receive a 401 with a WWW-Authenticate challenge. On success, the credential is stored in the context.

WARNING: Basic Auth credentials are base64-encoded, not encrypted. Always use this middleware behind HTTPS in production.

// Single user
app.Use(middleware.BasicAuth(middleware.BasicAuthConfig{
    Validator: middleware.Auth("alice", "s3cr3t"),
}))

// Multiple users
app.Use(middleware.BasicAuth(middleware.BasicAuthConfig{
    Validator: middleware.User{"alice": "s3cr3t", "bob": "p4ssw0rd"},
}))

// Custom validator (e.g. database lookup)
app.Use(middleware.BasicAuth(middleware.BasicAuthConfig{
    Realm: "Admin Area",
    Validator: middleware.ValidateFunc(func(_ shinobi.Ctx, c middleware.BasicAuthCredential) bool {
        return middleware.SecureCompare(c.Username, "alice") &&
            middleware.SecureCompare(c.Password, "s3cr3t")
    }),
}))

Retrieve the authenticated user in a handler with BasicAuthCredentialFrom:

app.Get("/profile", func(c shinobi.Ctx) error {
    cred, _ := middleware.BasicAuthCredentialFrom(c)
    return c.String(http.StatusOK, "hello, %s", cred.Username)
})
RateLimit

RateLimit limits the number of requests per IP using a sliding window algorithm. Requests that exceed the limit receive a 429 Too Many Requests through the error handler.

app.Use(middleware.RateLimit(100, time.Minute))
RealIP

RealIP sets the request's RemoteAddr to the real client IP extracted from the True-Client-IP, X-Real-IP, or X-Forwarded-For headers (in that order). The original port is preserved.

WARNING: Only use this middleware when requests come through a trusted reverse proxy. Without TrustedProxies, any client can forge these headers.

// Trust headers unconditionally — suitable only behind a controlled proxy
app.Use(middleware.RealIP())

// Restrict to known proxy CIDR ranges — recommended for production
app.Use(middleware.RealIPWithConfig(middleware.RealIPConfig{
    TrustedProxies: []string{"10.0.0.0/8", "192.168.1.0/24"},
}))

Place RealIP early in the middleware stack — before Logger, BasicAuth, and RateLimit — so that subsequent layers see the correct client IP.

WebSocket

Shinobi does not implement the WebSocket protocol. Instead it provides a minimal adapter interface so you can integrate any WebSocket library without coupling your application to a specific implementation.

go get github.com/nanoninja/shinobi/websocket
Interfaces
type Upgrader interface {
    Upgrade(w http.ResponseWriter, r *http.Request) (Conn, error)
}

type Conn interface {
    ReadMessage() (messageType int, p []byte, err error)
    WriteMessage(messageType int, data []byte) error
    Close() error
}
Message Type Constants
websocket.TextMessage   // 1
websocket.BinaryMessage // 2
websocket.CloseMessage  // 8
websocket.PingMessage   // 9
websocket.PongMessage   // 10
Usage with gorilla/websocket

Implement the Upgrader interface once for your chosen library:

import (
    "net/http"

    "github.com/gorilla/websocket"
    ws "github.com/nanoninja/shinobi/websocket"
)

type GorillaUpgrader struct {
    u *websocket.Upgrader
}

func NewGorillaUpgrader() *GorillaUpgrader {
    return &GorillaUpgrader{
        u: &websocket.Upgrader{
            CheckOrigin: func(r *http.Request) bool { return true },
        },
    }
}

func (g *GorillaUpgrader) Upgrade(w http.ResponseWriter, r *http.Request) (ws.Conn, error) {
    return g.u.Upgrade(w, r, nil)
}

Register the route with ws.Handle. The connection is automatically closed when the handler returns:

upgrader := NewGorillaUpgrader()

app.Get("/ws", ws.Handle(upgrader, func(c shinobi.Ctx, conn ws.Conn) error {
    for {
        msgType, msg, err := conn.ReadMessage()
        if err != nil {
            return err
        }
        if err := conn.WriteMessage(msgType, msg); err != nil {
            return err
        }
    }
}))

Error Handling

Handlers return errors that are caught by the framework. The default handler writes a 500 Internal Server Error. Override it with WithErrorHandler:

app := shinobi.New(
    shinobi.WithErrorHandler(func(err error, c shinobi.Ctx) {
        c.Response().Header().Set("Content-Type", "application/json")
        _ = c.JSON(http.StatusInternalServerError, map[string]string{
            "error": err.Error(),
        })
    }),
)
HTTPError

Use HTTPError to return a structured HTTP error directly from a handler. The default error handler will respond with the appropriate status code automatically.

app.Get("/users/{id}", func(c shinobi.Ctx) error {
    user, err := getUser(c.Param("id"))
    if err != nil {
        return shinobi.HTTPError(http.StatusNotFound, "user not found")
    }
    return c.JSON(http.StatusOK, user)
})

// without message — uses http.StatusText as fallback
return shinobi.HTTPError(http.StatusUnauthorized)

Handle *StatusError in a custom error handler to customize the response format:

shinobi.WithErrorHandler(func(err error, c shinobi.Ctx) {
    if e, ok := err.(*shinobi.StatusError); ok {
        _ = c.JSON(e.Code, map[string]any{"error": e.Message})
        return
    }
    _ = c.JSON(http.StatusInternalServerError, map[string]any{"error": "internal server error"})
})

Use WithInternal to wrap the original error for logging without exposing it to the client:

user, err := db.Find(c.Param("id"))
if err != nil {
    return shinobi.HTTPError(http.StatusNotFound, "user not found").WithInternal(err)
}

The internal cause is accessible via errors.Is and errors.As, and available on StatusError.Cause for logging in the error handler:

shinobi.WithErrorHandler(func(err error, c shinobi.Ctx) {
    if e, ok := err.(*shinobi.StatusError); ok {
        if e.Cause != nil {
            slog.Error("internal error", "cause", e.Cause)
        }
        _ = c.JSON(e.Code, map[string]any{"error": e.Message})
        return
    }
    _ = c.JSON(http.StatusInternalServerError, map[string]any{"error": "internal server error"})
})

You can define your own helpers on top of HTTPError:

func NotFound(msg ...any) error     { return shinobi.HTTPError(http.StatusNotFound, msg...) }
func Unauthorized(msg ...any) error { return shinobi.HTTPError(http.StatusUnauthorized, msg...) }
func BadRequest(msg ...any) error   { return shinobi.HTTPError(http.StatusBadRequest, msg...) }

HTML Templates

Provide a pre-loaded template renderer at startup. Shinobi integrates with github.com/nanoninja/render:

import (
    "github.com/nanoninja/render/tmpl"
    "github.com/nanoninja/render/tmpl/loader"
)

l := loader.NewFS(loader.LoaderConfig{Root: "templates", Extension: ".html"})

t := tmpl.NewHTML()
t.Load(l)

app := shinobi.New(shinobi.WithRenderer(t))

app.Get("/", func(c shinobi.Ctx) error {
    return c.HTML(http.StatusOK, "index", map[string]any{
        "Title": "Home",
    })
})

Server Lifecycle

// HTTP — blocking
app.Listen(":8080")

// HTTPS — blocking
app.ListenTLS(":443", "cert.pem", "key.pem")

// HTTP with graceful shutdown on SIGINT/SIGTERM
log.Fatal(app.ListenGraceful(":8080", 10*time.Second))

// HTTPS with graceful shutdown
log.Fatal(app.ListenTLSGraceful(":443", "cert.pem", "key.pem", 10*time.Second))

// Custom server — full control over TLS config, timeouts, etc.
srv := app.Server(":443")
srv.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS13}
srv.ReadTimeout = 5 * time.Second
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))

Versioning

Shinobi follows Semantic Versioning. While at v0.x, breaking changes may occur between minor versions — see the CHANGELOG for details before upgrading.

License

This project is licensed under the BSD 3-Clause License. See the LICENSE file for details.

Documentation

Overview

Package shinobi is a lightweight HTTP micro-framework built on top of the Go standard library.

Index

Constants

View Source
const (
	KB int64 = 1 << 10 // 1 024 bytes
	MB int64 = 1 << 20 // 1 048 576 bytes
	GB int64 = 1 << 30 // 1 073 741 824 bytes
)

Size constants for use with MaxSize, BodyLimit, and JSONMaxBytes. Example: shinobi.NewImageUpload(f, h, 5*shinobi.MB)

Variables

View Source
var DefaultBinder = BinderRegistry{
	"application/json":                  JSONBinder(),
	"application/xml":                   XMLBinder(),
	"application/x-www-form-urlencoded": FormBinder(),
}

DefaultBinder is the default BinderRegistry used when no custom Binder is configured. It supports application/json, application/xml, and application/x-www-form-urlencoded out of the box.

View Source
var (
	// ErrInvalidRedirectStatusCode is returned when an invalid HTTP status code is provided for redirection.
	ErrInvalidRedirectStatusCode = errors.New("invalid redirect status code")
)

HTTP Errors

View Source
var (
	// ErrTemplateRendererNotSet is returned when HTML is called without a configured template renderer.
	ErrTemplateRendererNotSet = errors.New("template renderer not configured")
)

Renderer errors

View Source
var (
	// ErrUnsupportedContentType is returned when no Binder is registered for the request Content-Type.
	ErrUnsupportedContentType = errors.New("unsupported content type")
)

Binder errors

View Source
var (
	// ErrValidatorNotSet is returned when Validate is called without a configured Validator.
	ErrValidatorNotSet = errors.New("validator not configured")
)

Validate errors

Functions

func JSONMaxBytes added in v0.3.0

func JSONMaxBytes(n int64) func(*jsonBinder)

JSONMaxBytes configures JSONBinder to limit the request body size to n bytes.

func JSONNumber added in v0.3.0

func JSONNumber() func(*jsonBinder)

JSONNumber configures JSONBinder to decode numbers as json.Number instead of float64.

func JSONStrict added in v0.3.0

func JSONStrict() func(*jsonBinder)

JSONStrict configures JSONBinder to reject unknown fields.

Types

type App

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

App is the top-level shinobi application. It wraps a Router and manages the HTTP server lifecycle.

func New

func New(opts ...Option) *App

New creates a new App with the given options. All options configure the underlying Router.

func (*App) Connect

func (a *App) Connect(pattern string, h HandlerFunc)

Connect registers a CONNECT route.

func (*App) Delete

func (a *App) Delete(pattern string, h HandlerFunc)

Delete registers a DELETE route.

func (*App) Get

func (a *App) Get(pattern string, h HandlerFunc)

Get registers a GET route.

func (*App) Group

func (a *App) Group(prefix string, fn func(Router))

Group creates a sub-router with a prefix for all routes registered within fn.

func (*App) Handle

func (a *App) Handle(pattern string, h Handler)

Handle registers a new route with a handler for the given pattern.

func (*App) HandleFunc

func (a *App) HandleFunc(pattern string, h HandlerFunc)

HandleFunc registers a new route with a handler function for the given pattern.

func (*App) Head

func (a *App) Head(pattern string, h HandlerFunc)

Head registers a HEAD route.

func (*App) IsDebug added in v0.11.0

func (a *App) IsDebug() bool

IsDebug reports whether the application is running in debug mode.

func (*App) Listen

func (a *App) Listen(addr string) error

Listen starts an HTTP server on addr and blocks until it stops.

func (*App) ListenGraceful

func (a *App) ListenGraceful(addr string, timeout time.Duration) error

ListenGraceful starts an HTTP server and shuts it down gracefully on SIGINT or SIGTERM.

func (*App) ListenTLS

func (a *App) ListenTLS(addr, certFile, keyFile string) error

ListenTLS starts an HTTPS server with the given certificate and key files.

func (*App) ListenTLSGraceful

func (a *App) ListenTLSGraceful(addr, certFile, keyFile string, timeout time.Duration) error

ListenTLSGraceful starts an HTTPS server and shuts it down gracefully on SIGINT or SIGTERM.

func (*App) Method

func (a *App) Method(method, pattern string, h Handler)

Method registers a new route with a specific HTTP method and handler.

func (*App) MethodFunc

func (a *App) MethodFunc(method, pattern string, h HandlerFunc)

MethodFunc registers a new route with a specific HTTP method and handler function.

func (*App) Mount

func (a *App) Mount(prefix string, h Handler)

Mount attaches a Handler at the given prefix.

func (*App) Name added in v0.7.0

func (a *App) Name() string

Name returns the application name used in log messages.

func (*App) NotFound

func (a *App) NotFound(h HandlerFunc)

NotFound registers a custom handler for 404 responses.

func (*App) Options

func (a *App) Options(pattern string, h HandlerFunc)

Options registers an OPTIONS route.

func (*App) Patch

func (a *App) Patch(pattern string, h HandlerFunc)

Patch registers a PATCH route.

func (*App) Post

func (a *App) Post(pattern string, h HandlerFunc)

Post registers a POST route.

func (*App) Put

func (a *App) Put(pattern string, h HandlerFunc)

Put registers a PUT route.

func (*App) Route

func (a *App) Route(pattern string, fn func(Route))

Route registers multiple HTTP methods on a single pattern.

func (*App) Router

func (a *App) Router() Router

Router returns the underlying Router for advanced configuration.

func (*App) Routes

func (a *App) Routes() []RouteInfo

Routes returns all registered routes for introspection.

func (*App) ServeHTTP

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler, delegating to the underlying router.

func (*App) Server

func (a *App) Server(addr string) *http.Server

Server returns an http.Server pre-configured with production-safe defaults: ReadHeaderTimeout 5s, ReadTimeout 10s, WriteTimeout 30s, IdleTimeout 120s. Any field can be overridden after calling this method.

func (*App) SetDebug added in v0.11.0

func (a *App) SetDebug(v bool)

SetDebug enables or disables debug mode. In debug mode, routes are logged at startup and error responses include the original error message.

func (*App) SetLogger added in v0.11.0

func (a *App) SetLogger(l *slog.Logger)

SetLogger sets the logger used by the application and injected into each request context. Ignored if l is nil. Defaults to slog.Default().

func (*App) SetName added in v0.7.0

func (a *App) SetName(name string)

SetName sets the application name used in log messages. Ignored if name is empty. Defaults to "shinobi".

func (*App) Trace

func (a *App) Trace(pattern string, h HandlerFunc)

Trace registers a TRACE route.

func (*App) Use

func (a *App) Use(middlewares ...Middleware)

Use appends one or more middlewares to the global middleware stack.

func (*App) With

func (a *App) With(middlewares ...Middleware) Router

With returns a new Router scoped with the provided middlewares.

type Binder added in v0.3.0

type Binder interface {
	Bind(r *http.Request, v any) error
}

Binder decodes an HTTP request body into a value.

func FormBinder added in v0.5.0

func FormBinder() Binder

FormBinder returns a Binder that decodes form fields into a struct using `form:""` field tags. Supported types: string, int, int64, float64, bool, time.Time. Use `format:""` to specify a custom time layout. Pointer fields are treated as optional — nil when the field is absent.

func JSONBinder added in v0.3.0

func JSONBinder(opts ...func(*jsonBinder)) Binder

JSONBinder returns a Binder that decodes JSON request bodies.

func QueryBinder added in v0.5.0

func QueryBinder() Binder

QueryBinder returns a Binder that decodes URL query parameters into a struct using `query:""` field tags. Supported types: string, int, int64, float64, bool, time.Time. Use `format:""` to specify a custom time layout. Pointer fields are treated as optional — nil when the parameter is absent.

func XMLBinder added in v0.3.0

func XMLBinder() Binder

XMLBinder returns a Binder that decodes XML request bodies.

type BinderRegistry added in v0.3.0

type BinderRegistry map[string]Binder

BinderRegistry maps MIME type prefixes to their corresponding Binder. It implements Binder itself, selecting the appropriate decoder based on Content-Type.

func (BinderRegistry) Bind added in v0.3.0

func (bg BinderRegistry) Bind(r *http.Request, v any) error

Bind selects the appropriate Binder based on the request Content-Type and decodes the body into v.

type Config added in v0.11.0

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

Config holds the configuration for an App and its Router. Use functional Options to customize it; most callers rely on DefaultConfig.

func DefaultConfig added in v0.11.0

func DefaultConfig() *Config

DefaultConfig returns a Config with production-safe defaults.

type Ctx

type Ctx interface {
	// Request returns the underlying HTTP request.
	Request() *http.Request

	// Response returns the wrapped HTTP response writer.
	Response() *Response

	// Context returns the request's stdlib context.
	Context() context.Context

	// WithContext returns a shallow copy of Ctx with the request context replaced.
	WithContext(ctx context.Context) Ctx

	// Set stores a value in the request context chain.
	Set(key, value any)

	// Get retrieves a value from the request context chain.
	Get(key any) (any, bool)

	// Logger returns the logger associated with this request context.
	Logger() *slog.Logger

	// Debug reports whether the application is running in debug mode.
	Debug() bool

	// IsLoopback reports whether addr is a loopback address.
	IsLoopback(addr string) bool

	// IsSecure reports whether the request was made over HTTPS.
	IsSecure() bool

	// IsXHR reports whether the request was made via XMLHttpRequest.
	IsXHR() bool

	// IsWebSocket reports whether the request is a WebSocket upgrade.
	IsWebSocket() bool

	// IsMethod reports whether the request method matches m (case-insensitive).
	IsMethod(s string) bool

	// Method returns the HTTP method of the request (e.g. "GET", "POST").
	Method() string

	// Path returns the URL path of the request (e.g. "/users/42").
	Path() string

	// Host returns the host from the request (e.g. "example.com").
	Host() string

	// Scheme returns "https" if the request is secure, "http" otherwise.
	Scheme() string

	// Query returns the first value for the named query parameter.
	Query(key string) string

	// QueryValues returns all query parameters as url.Values.
	QueryValues() url.Values

	// FormValue returns the first value for the named form field.
	FormValue(key string) string

	// ParseForm populates Request.Form and Request.PostForm.
	ParseForm() error

	// Param returns the path parameter value for the given key.
	Param(key string) string

	// ContentType returns the Content-Type header of the request.
	ContentType() string

	// UserAgent returns the User-Agent header of the request.
	UserAgent() string

	// Referer returns the Referer header of the request.
	Referer() string

	// FormFile returns the uploaded file for the named form key.
	FormFile(key string) (multipart.File, *multipart.FileHeader, error)

	// Bind decodes the request body into v based on the Content-Type header.
	Bind(v any) error

	// BindQuery decodes URL query parameters into v using struct field tags.
	// Fields are mapped via the `query:""` tag. Supported types: string, int,
	// int64, float64, bool, time.Time. Use `format:""` to specify a custom time
	// layout; otherwise RFC3339, DateTime and DateOnly are tried in order.
	// Pointer fields are optional — nil when the parameter is absent.
	// Fields without a tag or with tag `query:"-"` are ignored.
	//
	// Example:
	//
	//	type SearchParams struct {
	//	    Query string    `query:"q"`
	//	    Page  int       `query:"page"`
	//	    Since time.Time `query:"since"`
	//	}
	//	var p SearchParams
	//	if err := c.BindQuery(&p); err != nil {
	//	    return err
	//	}
	BindQuery(v any) error

	// BindForm decodes form fields from the request body into v using `form:""` struct tags.
	// Supported types: string, int, int64, float64, bool, time.Time. Use `format:""`
	// to specify a custom time layout; otherwise RFC3339, DateTime and DateOnly are tried in order.
	// Pointer fields are optional — nil when the field is absent.
	// Fields without a tag or with tag `form:"-"` are ignored.
	BindForm(v any) error

	// Validate validates v using the configured Validator.
	Validate(v any) error

	// SetCookie adds a Set-Cookie header to the response.
	SetCookie(cookie *http.Cookie)

	// DeleteCookie expires a cookie by name and path.
	DeleteCookie(name, path string)

	// SetHeader sets a response header, replacing any existing value.
	SetHeader(key, value string)

	// AddHeader adds a response header value without replacing existing ones.
	AddHeader(key, value string)

	// Redirect sends an HTTP redirect. Returns an error if code is not 3xx.
	Redirect(url string, code int) error

	// NoContent sends a 204 No Content response.
	NoContent() error

	// Render writes data to the response using the given renderer.
	Render(code int, r render.Renderer, data any, opts ...func(*render.Options)) error

	// String sends a plain text response, formatted if args are provided.
	String(code int, s string, args ...any) error

	// JSON sends a JSON-encoded response.
	JSON(code int, data any, opts ...func(*render.Options)) error

	// XML sends an XML-encoded response.
	XML(code int, data any, opts ...func(*render.Options)) error

	// HTML renders a named template with the pre-loaded template renderer.
	HTML(code int, name string, data any, opts ...func(*render.Options)) error

	// CSV renders a 2D string slice as a CSV response.
	CSV(code int, data [][]string, opts ...func(*render.Options)) error

	// Blob sends raw binary data with the given content type via the render package.
	Blob(code int, data []byte, opts ...func(*render.Options)) error

	// File serves a file from the given path using http.ServeContent.
	File(path string) error

	// Attachment serves a file as a downloadable attachment with Content-Disposition.
	Attachment(path string) error

	// Inline serves a file inline in the browser with Content-Disposition.
	Inline(path string) error
}

Ctx represents the context of an HTTP request/response cycle. It wraps http.Request and http.ResponseWriter and provides a unified API for handlers and middleware.

func NewCtx

func NewCtx(w http.ResponseWriter, req *http.Request, cfg *Config) Ctx

NewCtx creates a new Ctx wrapping the given ResponseWriter, Request and Config.

type ErrorHandler

type ErrorHandler func(err error, c Ctx)

ErrorHandler handles errors returned by a Handler.

type FileUpload added in v0.12.1

type FileUpload struct {
	File              multipart.File
	Header            *multipart.FileHeader
	MaxSize           int64               // maximum allowed size in bytes; 0 disables the check
	AllowedTypes      []string            // accepted MIME type prefixes; nil disables the check
	AllowedExtensions []string            // accepted extensions e.g. ".jpg"; nil disables the check
	DetectFunc        func([]byte) string // custom MIME detector; nil falls back to http.DetectContentType
}

FileUpload holds a multipart file and its metadata, and implements Validatable. Pass it to Ctx.Validate to enforce size, MIME type, and extension constraints.

Size is measured by reading the actual file body — not from Header.Size which is declared by the client and cannot be trusted. The file is seeked back to the start after measurement.

MIME type is detected from the first 512 bytes of the file content. By default http.DetectContentType is used (W3C MIME sniffing), which covers common formats but not all — SVG for instance is detected as text/xml. Provide a custom DetectFunc to use a more complete detection library.

AllowedExtensions is checked against the filename declared by the client. It is spoofable on its own — always combine with AllowedTypes for reliable validation.

func NewDocumentUpload added in v0.12.1

func NewDocumentUpload(file multipart.File, header *multipart.FileHeader, maxSize int64) *FileUpload

NewDocumentUpload creates a FileUpload preset for PDF documents. maxSize is the maximum allowed file size in bytes.

func NewFileUploadValidator added in v0.12.1

func NewFileUploadValidator(file multipart.File, header *multipart.FileHeader) *FileUpload

NewFileUploadValidator creates a bare FileUpload from the result of Ctx.FormFile. No constraints are set — configure MaxSize, AllowedTypes, AllowedExtensions, and DetectFunc directly on the returned value, or use a preset constructor.

func NewImageUpload added in v0.12.1

func NewImageUpload(file multipart.File, header *multipart.FileHeader, maxSize int64) *FileUpload

NewImageUpload creates a FileUpload preset for common web image formats (JPEG, PNG, GIF, WebP). maxSize is the maximum allowed file size in bytes.

func (*FileUpload) AddExtension added in v0.12.1

func (fu *FileUpload) AddExtension(exts ...string) *FileUpload

AddExtension appends one or more file extensions to the allowed extensions list. Extensions should include the leading dot (e.g. ".svg"). Returns the FileUpload to allow method chaining.

func (*FileUpload) AddType added in v0.12.1

func (fu *FileUpload) AddType(t ...string) *FileUpload

AddType appends one or more MIME type prefixes to the allowed types list. Returns the FileUpload to allow method chaining.

func (*FileUpload) Validate added in v0.12.1

func (fu *FileUpload) Validate() error

Validate checks the file against the configured constraints in order: actual size, MIME type, and file extension. It implements Validatable and is called automatically by Ctx.Validate.

type Handler

type Handler interface {
	Handle(c Ctx) error
}

Handler is the core interface for handling an HTTP request within a shinobi context.

func AdaptHTTP

func AdaptHTTP(h http.Handler) Handler

AdaptHTTP wraps a stdlib http.Handler as a shinobi Handler. Use it to mount external handlers (file servers, third-party routers) via Mount.

func FileServer

func FileServer(dir string) Handler

FileServer returns a Handler that serves static files from the given directory. Designed to be used with Mount.

Example:

r.Mount("/assets", shinobi.FileServer("./public"))

func FileServerFS

func FileServerFS(fsys fs.FS) Handler

FileServerFS returns a Handler that serves static files from the given fs.FS. Useful with Go's embed package.

Example:

//go:embed public
var public embed.FS

r.Mount("/assets", shinobi.FileServerFS(public))

type HandlerFunc

type HandlerFunc func(c Ctx) error

HandlerFunc is a function adapter that implements Handler.

func (HandlerFunc) Handle

func (f HandlerFunc) Handle(c Ctx) error

Handle calls f(c) and returns its error.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler to extend or intercept request processing.

func Adapt

func Adapt(m func(http.Handler) http.Handler) Middleware

Adapt converts a stdlib middleware (func(http.Handler) http.Handler) into a shinobi Middleware. Context changes made by the stdlib middleware (e.g. r.WithContext) are propagated to the shinobi Ctx.

type Option

type Option func(*Config)

Option defines a function type for configuring the Config.

func WithBinder added in v0.3.0

func WithBinder(b Binder) Option

WithBinder sets a custom Binder used to decode request bodies in Ctx.Bind.

func WithDebug added in v0.11.0

func WithDebug(v bool) Option

WithDebug enables or disables debug mode injected into each Ctx. In debug mode, error responses include the original error message.

func WithErrorHandler

func WithErrorHandler(h ErrorHandler) Option

WithErrorHandler sets a custom handler for errors returned by route handlers.

func WithLogger added in v0.11.0

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger injected into each Ctx. Ignored if l is nil. Defaults to slog.Default().

func WithMux

func WithMux(mux *http.ServeMux) Option

WithMux allows providing a custom http.ServeMux instance.

func WithNotFound

func WithNotFound(h HandlerFunc) Option

WithNotFound sets a custom handler for 404 Not Found errors during initialization.

func WithPrefix

func WithPrefix(prefix string) Option

WithPrefix sets an initial global prefix for all routes.

func WithRenderer added in v0.3.0

func WithRenderer(r render.Renderer) Option

WithRenderer sets the template renderer injected into each Ctx.

func WithValidator added in v0.4.0

func WithValidator(v Validator) Option

WithValidator sets the validator injected into each Ctx.

type Response

type Response struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

Response wraps http.ResponseWriter to capture the HTTP status code and guard against writing headers more than once.

func NewResponse

func NewResponse(w http.ResponseWriter) *Response

NewResponse returns a Response with a default 200 OK status.

func (*Response) SetResponseWriter added in v0.5.0

func (r *Response) SetResponseWriter(fn func(http.ResponseWriter) http.ResponseWriter)

SetResponseWriter replaces the underlying ResponseWriter using a wrapping function. The function receives the current writer, ensuring the chain is never broken.

func (*Response) Status

func (r *Response) Status() int

Status returns the captured HTTP status code.

func (*Response) Unwrap

func (r *Response) Unwrap() http.ResponseWriter

Unwrap returns the underlying http.ResponseWriter.

func (*Response) Write

func (r *Response) Write(b []byte) (int, error)

Write sends the response body, writing a 200 OK header first if not yet sent.

func (*Response) WriteHeader

func (r *Response) WriteHeader(code int)

WriteHeader sends the HTTP status code once, ignoring subsequent calls.

func (*Response) WriteString

func (r *Response) WriteString(s string) (int, error)

WriteString writes a string to the response body.

func (*Response) Written

func (r *Response) Written() bool

Written reports whether the response headers have already been sent.

type Route

type Route interface {
	Connect(h HandlerFunc)
	Delete(h HandlerFunc)
	Get(h HandlerFunc)
	Head(h HandlerFunc)
	Options(h HandlerFunc)
	Patch(h HandlerFunc)
	Post(h HandlerFunc)
	Put(h HandlerFunc)
	Trace(h HandlerFunc)
}

Route defines multi-method registration on a single pattern.

type RouteInfo

type RouteInfo struct {
	Method  string // HTTP method (e.g. "GET", "POST")
	Pattern string // Full resolved path (e.g. "/api/v1/users/{id}")
}

RouteInfo holds the HTTP method and fully resolved pattern of a registered route. It is returned by Router.Routes() for introspection and debugging purposes.

type Router

type Router interface {
	http.Handler

	// Handle registers a new route with a handler for the given pattern.
	Handle(pattern string, h Handler)

	// HandleFunc registers a new route with a handler function.
	HandleFunc(pattern string, h HandlerFunc)

	// Method registers a new route with a specific HTTP method and handler.
	Method(method, pattern string, h Handler)

	// MethodFunc registers a new route with a specific HTTP method and handler function.
	MethodFunc(method, pattern string, h HandlerFunc)

	// HTTP Verb shortcuts.
	Connect(pattern string, h HandlerFunc)
	Delete(pattern string, h HandlerFunc)
	Get(pattern string, h HandlerFunc)
	Head(pattern string, h HandlerFunc)
	Options(pattern string, h HandlerFunc)
	Patch(pattern string, h HandlerFunc)
	Post(pattern string, h HandlerFunc)
	Put(pattern string, h HandlerFunc)
	Trace(pattern string, h HandlerFunc)

	// Group creates a new sub-router with a prefix.
	// All routes registered within the provided function will inherit this prefix.
	Group(prefix string, fn func(Router))

	// Mount attaches a shinobi Handler at the given prefix.
	// The mounted handler receives requests with the prefix stripped from the path.
	// Use AdaptHTTP to mount stdlib http.Handler implementations.
	Mount(prefix string, h Handler)

	// Use appends one or more middlewares to the router's global middleware stack.
	Use(...Middleware)

	Route(pattern string, fn func(Route))

	// Routes returns all registered routes.
	Routes() []RouteInfo

	// With returns a new Router instance that includes the provided middlewares
	// in addition to the existing ones. Perfect for method chaining.
	With(middlewares ...Middleware) Router

	// NotFound registers a custom handler for 404 Not Found errors.
	NotFound(h HandlerFunc)
}

Router defines the interface for a layered HTTP router.

func NewRouter

func NewRouter(opts ...Option) Router

NewRouter creates a new Router instance with optional configurations.

type StatusError added in v0.5.0

type StatusError struct {
	Code    int
	Message any
	Cause   error
}

StatusError represents an HTTP error with a status code and message.

func HTTPError added in v0.5.0

func HTTPError(code int, message ...any) *StatusError

HTTPError creates a new StatusError with the given HTTP status code and optional message.

func (*StatusError) Error added in v0.5.0

func (e *StatusError) Error() string

func (*StatusError) Unwrap added in v0.5.0

func (e *StatusError) Unwrap() error

Unwrap returns the internal cause, enabling errors.Is and errors.As traversal.

func (*StatusError) WithInternal added in v0.5.0

func (e *StatusError) WithInternal(err error) *StatusError

WithInternal wraps an internal error for logging without exposing it to the client.

type Validatable added in v0.12.1

type Validatable interface {
	Validate() error
}

Validatable may be implemented by any value that knows how to validate itself. If a value passed to Ctx.Validate implements this interface, its Validate method is called directly, bypassing the globally configured Validator. This is useful for types that carry their own validation logic (e.g. FileUpload) without depending on the application-level validator.

type Validator added in v0.4.0

type Validator interface {
	Validate(v any) error
}

Validator validates a decoded value. No default implementation is provided — plug in the library of your choice (e.g. go-playground/validator) via WithValidator.

Directories

Path Synopsis
_example
basic command
bind command
errors command
middleware command
profiling command
Profiling example — exposes pprof endpoints behind BasicAuth.
Profiling example — exposes pprof endpoints behind BasicAuth.
renderer command
routing command
security command
Security example — demonstrates BasicAuth, SecureHeaders, CORS and BodyLimit.
Security example — demonstrates BasicAuth, SecureHeaders, CORS and BodyLimit.
static command
upload command
validate command
Package middleware provides built-in HTTP middlewares for the Shinobi framework.
Package middleware provides built-in HTTP middlewares for the Shinobi framework.
Package websocket provides an interface for integrating any WebSocket library with Shinobi.
Package websocket provides an interface for integrating any WebSocket library with Shinobi.

Jump to

Keyboard shortcuts

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