handlerx

package module
v1.0.3 Latest Latest
Warning

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

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

README

HandlerX

HandlerX is a lightweight, framework-agnostic HTTP handler abstraction for Go.

It allows you to write your business logic once and plug it into different HTTP frameworks like Fiber, Echo, or even the standard net/http — without rewriting your handlers.


✨ Features

  • 🔌 Framework-agnostic handlers
  • 🧩 Simple and composable API
  • 🔁 Middleware support (Next())
  • 📦 Unified response model
  • 🧪 Easy to test (mockable context)
  • ⚡ Minimal and idiomatic Go

📦 Installation

go get github.com/atendi9/handlerx

🚀 Basic Example

func HelloHandler(c handlerx.Context) handlerx.Response {
	name := c.Query("name", "guest")

	return handlerx.Response{
		Data: map[string]string{
			"message": "Hello " + name,
		},
	}
}

🔌 Integrations

⚡ Fiber
package main

import (
	"mime/multipart"
	"time"

	"github.com/atendi9/handlerx"
	"github.com/gofiber/fiber/v2"
)

// ===== Context Implementation =====

type FiberContext struct {
	Ctx *fiber.Ctx
}

func (f FiberContext) Headers() map[string][]string {
	return f.Ctx.GetReqHeaders()
}

func (f FiberContext) BodyParser(v any) error {
	return f.Ctx.BodyParser(v)
}

func (f FiberContext) QueryParser(v any) error {
	return f.Ctx.QueryParser(v)
}

func (f FiberContext) ParamsParser(v any) error {
	return nil
}

func (f FiberContext) ReqHeaderParser(v any) error {
	return nil
}

func (f FiberContext) Header(key string) string {
	return f.Ctx.Get(key)
}

func (f FiberContext) Method() string {
	return f.Ctx.Method()
}

func (f FiberContext) IP() string {
	return f.Ctx.IP()
}

func (f FiberContext) IPs() []string {
	return f.Ctx.IPs()
}

func (f FiberContext) Body() []byte {
	return f.Ctx.Body()
}

func (f FiberContext) Query(name string, defaultValue ...string) string {
	if len(defaultValue) > 0 {
		return f.Ctx.Query(name, defaultValue[0])
	}
	return f.Ctx.Query(name)
}

func (f FiberContext) Params(name string, defaultValue ...string) string {
	if len(defaultValue) > 0 {
		return f.Ctx.Params(name, defaultValue[0])
	}
	return f.Ctx.Params(name)
}

func (f FiberContext) FormFile(key string) (*multipart.FileHeader, error) {
	return f.Ctx.FormFile(key)
}

func (f FiberContext) SendStatus(status int) error {
	return f.Ctx.SendStatus(status)
}

func (f FiberContext) Send(data []byte) error {
	return f.Ctx.Send(data)
}

func (f FiberContext) JSON(data any) error {
	return f.Ctx.JSON(data)
}

func (f FiberContext) Next() error {
	return f.Ctx.Next()
}

func (f FiberContext) Now() time.Time {
	return time.Now()
}

func (f FiberContext) Path(defaultValue ...string) string {
	return f.Ctx.Path()
}

// ===== Converter =====

type FiberConverter struct{}

func (f FiberConverter) Convert(h handlerx.Handler) fiber.Handler {
	return func(c *fiber.Ctx) error {
		ctx := FiberContext{Ctx: c}
		res := h(handlerx.Atendi9Context{Context: ctx})

		if res.GoNext() {
			return c.Next()
		}

		if len(res.FilePath) > 0 {
			return c.SendFile(res.FilePath)
		}

		if err := res.Err; err != nil {
			return c.Status(res.Status()).JSON(fiber.Map{
				"err": err.Error(),
			})
		}

		if v, ok := res.Data.(string); ok {
			return c.Status(res.Status()).SendString(v)
		}

		return c.Status(res.Status()).JSON(res.Data)
	}
}

// ===== Handler =====

func Hello(c handlerx.Context) handlerx.Response {
	return handlerx.Response{
		Data: map[string]string{
			"message": "Hello from Fiber",
		},
	}
}

// ===== Main =====

func main() {
	app := fiber.New()
	conv := FiberConverter{}

	app.Get("/", conv.Convert(Hello))

	app.Listen(":3000")
}

🌐 Echo
package main

import (
	"mime/multipart"
	"net/http"
	"time"

	"github.com/atendi9/handlerx"
	"github.com/labstack/echo/v4"
)

// ===== Context =====

type EchoContext struct {
	Ctx echo.Context
}

func (e EchoContext) Headers() map[string][]string {
	return e.Ctx.Request().Header
}

func (e EchoContext) BodyParser(v any) error {
	return e.Ctx.Bind(v)
}

func (e EchoContext) QueryParser(v any) error {
	return e.Ctx.Bind(v)
}

func (e EchoContext) ParamsParser(v any) error {
	return e.Ctx.Bind(v)
}

func (e EchoContext) ReqHeaderParser(v any) error {
	return nil
}

func (e EchoContext) Header(key string) string {
	return e.Ctx.Request().Header.Get(key)
}

func (e EchoContext) Method() string {
	return e.Ctx.Request().Method
}

func (e EchoContext) IP() string {
	return e.Ctx.RealIP()
}

func (e EchoContext) IPs() []string {
	return []string{e.Ctx.RealIP()}
}

func (e EchoContext) Body() []byte {
	return nil
}

func (e EchoContext) Query(name string, defaultValue ...string) string {
	val := e.Ctx.QueryParam(name)
	if val == "" && len(defaultValue) > 0 {
		return defaultValue[0]
	}
	return val
}

func (e EchoContext) Params(name string, defaultValue ...string) string {
	val := e.Ctx.Param(name)
	if val == "" && len(defaultValue) > 0 {
		return defaultValue[0]
	}
	return val
}

func (e EchoContext) FormFile(key string) (*multipart.FileHeader, error) {
	return e.Ctx.FormFile(key)
}

func (e EchoContext) SendStatus(status int) error {
	return e.Ctx.NoContent(status)
}

func (e EchoContext) Send(data []byte) error {
	return e.Ctx.Blob(http.StatusOK, "application/octet-stream", data)
}

func (e EchoContext) JSON(data any) error {
	return e.Ctx.JSON(http.StatusOK, data)
}

func (e EchoContext) Next() error {
	return nil
}

func (e EchoContext) Now() time.Time {
	return time.Now()
}

func (e EchoContext) Path(defaultValue ...string) string {
	return e.Ctx.Path()
}

// ===== Converter =====

type EchoConverter struct{}

func (e EchoConverter) Convert(h handlerx.Handler) echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx := EchoContext{Ctx: c}
		res := h(handlerx.Atendi9Context{Context: ctx})

		if res.GoNext() {
			return nil
		}

		if len(res.FilePath) > 0 {
			return c.File(res.FilePath)
		}

		if err := res.Err; err != nil {
			return c.JSON(res.Status(), map[string]string{
				"err": err.Error(),
			})
		}

		if v, ok := res.Data.(string); ok {
			return c.String(res.Status(), v)
		}

		return c.JSON(res.Status(), res.Data)
	}
}

// ===== Handler =====

func Hello(c handlerx.Context) handlerx.Response {
	return handlerx.Response{
		Data: map[string]string{
			"message": "Hello from Echo",
		},
	}
}

// ===== Main =====

func main() {
	e := echo.New()
	conv := EchoConverter{}

	e.GET("/", conv.Convert(Hello))

	e.Start(":3000")
}


🧱 net/http (Standard Library)
package main

import (
	"encoding/json"
	"mime/multipart"
	"net/http"
	"time"

	"github.com/atendi9/handlerx"
)

// ===== Context =====

type HTTPContext struct {
	Req *http.Request
	Res http.ResponseWriter
}

func (h HTTPContext) Headers() map[string][]string {
	return h.Req.Header
}

func (h HTTPContext) BodyParser(v any) error {
	return json.NewDecoder(h.Req.Body).Decode(v)
}

func (h HTTPContext) QueryParser(v any) error {
	return nil
}

func (h HTTPContext) ParamsParser(v any) error {
	return nil
}

func (h HTTPContext) ReqHeaderParser(v any) error {
	return nil
}

func (h HTTPContext) Header(key string) string {
	return h.Req.Header.Get(key)
}

func (h HTTPContext) Method() string {
	return h.Req.Method
}

func (h HTTPContext) IP() string {
	return h.Req.RemoteAddr
}

func (h HTTPContext) IPs() []string {
	return []string{h.Req.RemoteAddr}
}

func (h HTTPContext) Body() []byte {
	return nil
}

func (h HTTPContext) Query(name string, defaultValue ...string) string {
	val := h.Req.URL.Query().Get(name)
	if val == "" && len(defaultValue) > 0 {
		return defaultValue[0]
	}
	return val
}

func (h HTTPContext) Params(name string, defaultValue ...string) string {
	return ""
}

func (h HTTPContext) FormFile(key string) (*multipart.FileHeader, error) {
	return nil, nil
}

func (h HTTPContext) SendStatus(status int) error {
	h.Res.WriteHeader(status)
	return nil
}

func (h HTTPContext) Send(data []byte) error {
	h.Res.Write(data)
	return nil
}

func (h HTTPContext) JSON(data any) error {
	h.Res.Header().Set("Content-Type", "application/json")
	return json.NewEncoder(h.Res).Encode(data)
}

func (h HTTPContext) Next() error {
	return nil
}

func (h HTTPContext) Now() time.Time {
	return time.Now()
}

func (h HTTPContext) Path(defaultValue ...string) string {
	return h.Req.URL.Path
}

// ===== Converter =====

type HTTPConverter struct{}

func (h HTTPConverter) Convert(fn handlerx.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := HTTPContext{Req: r, Res: w}
		res := fn(handlerx.Atendi9Context{Context: ctx})

		if res.GoNext() {
			return
		}

		if len(res.FilePath) > 0 {
			http.ServeFile(w, r, res.FilePath)
			return
		}

		// ===== ERROR =====
		if err := res.Err; err != nil {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(res.Status())
			json.NewEncoder(w).Encode(map[string]string{
				"err": err.Error(),
			})
			return
		}

		// ===== STRING =====
		if v, ok := res.Data.(string); ok {
			w.Header().Set("Content-Type", "text/plain; charset=utf-8")
			w.WriteHeader(res.Status())
			w.Write([]byte(v))
			return
		}

		// ===== JSON =====
		w.Header().Set("Content-Type", "application/json") // FIX
		w.WriteHeader(res.Status())
		json.NewEncoder(w).Encode(res.Data)
	}
}

// ===== Handler =====

func Hello(c handlerx.Context) handlerx.Response {
	return handlerx.Response{
		Data: map[string]string{
			"message": "Hello from net/http",
		},
	}
}

// ===== Main =====

func main() {
	mux := http.NewServeMux()
	conv := HTTPConverter{}

	mux.HandleFunc("/", conv.Convert(Hello))

	http.ListenAndServe(":3000", mux)
}

🧠 Philosophy

HandlerX separates:

  • Transport layer (Fiber, Echo, HTTP)
  • Business logic (your handlers)

This makes your code:

  • Easier to test 🧪
  • Easier to migrate 🔄
  • Easier to maintain 🧼

📄 License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Atendi9Context

type Atendi9Context struct {
	Context
}

Atendi9Context is a wrapper around Context that allows extending or customizing behavior without modifying the original implementation.

It can be used to add helper methods specific to your application.

func NewContext

func NewContext(ctx Context) *Atendi9Context

NewContext creates a new Atendi9Context wrapping the given Context.

Example:

ctx := NewContext(originalCtx)

func (*Atendi9Context) Test

func (c *Atendi9Context) Test(ctx Context) *Atendi9Context

Test replaces the underlying Context instance.

This method is primarily useful for testing or dynamically swapping context implementations.

Example:

ctx.Test(mockCtx)

type Context

type Context interface {
	// Headers returns all request headers.
	//
	// The returned map follows the standard Go format:
	// map[string][]string.
	Headers() map[string][]string

	// BodyParser parses the request body into the given struct.
	//
	// It supports formats such as JSON, XML, or form data,
	// depending on the implementation.
	//
	// Example:
	//	var req MyStruct
	//	if err := c.BodyParser(&req); err != nil {
	//	    return err
	//	}
	BodyParser(v any) error

	// QueryParser parses query string parameters into the given struct.
	//
	// Example:
	//	var query QueryParams
	//	_ = c.QueryParser(&query)
	QueryParser(v any) error

	// ParamsParser parses route (path) parameters into the given struct.
	//
	// Example:
	//	// Route: /users/:id
	//	var params struct { ID string `param:"id"` }
	//	_ = c.ParamsParser(&params)
	ParamsParser(v any) error

	// ReqHeaderParser parses request headers into the given struct.
	//
	// This is useful for binding headers to typed structures.
	ReqHeaderParser(v any) error

	// Header returns the value of a specific request header.
	//
	// If the header does not exist, an empty string is returned.
	Header(key string) string

	// Method returns the HTTP method used in the request
	// (e.g., GET, POST, PUT, DELETE).
	Method() string

	// IP returns the client IP address.
	//
	// Depending on the implementation, this may consider proxy headers
	// such as X-Forwarded-For.
	IP() string

	// IPs returns all IP addresses associated with the request,
	// including proxy chain addresses.
	IPs() []string

	// Body returns the raw request body.
	//
	// Useful when manual parsing is needed.
	Body() []byte

	// Query returns the value of a query parameter.
	//
	// If the parameter is not present, it returns the optional default value.
	//
	// Example:
	//	name := c.Query("name", "guest")
	Query(name string, defaultValue ...string) string

	// Params returns the value of a route (path) parameter.
	//
	// If the parameter is not present, it returns the optional default value.
	//
	// Example:
	//	id := c.Params("id", "0")
	Params(name string, defaultValue ...string) string

	// FormFile retrieves a file uploaded via multipart form.
	//
	// Returns a pointer to multipart.FileHeader, which can be used
	// to open and read the file.
	//
	// Example:
	//	file, err := c.FormFile("avatar")
	FormFile(key string) (*multipart.FileHeader, error)

	// SendStatus sets the HTTP status code and sends the response.
	//
	// Example:
	//	return c.SendStatus(404)
	SendStatus(status int) error

	// Send writes raw bytes as the response body.
	//
	// Example:
	//	return c.Send([]byte("ok"))
	Send(data []byte) error

	// JSON serializes the given data as JSON and writes it to the response.
	//
	// Example:
	//	return c.JSON(map[string]string{"status": "ok"})
	JSON(data any) error

	// Next passes control to the next middleware/handler in the chain.
	//
	// Commonly used in middleware pipelines.
	Next() error

	// Now returns the current time.
	//
	// This abstraction allows easier testing by mocking time.
	Now() time.Time

	// Path returns the request path.
	//
	// If no path is available, it may return the optional default value.
	Path(defaultValue ...string) string
}

Context defines an abstraction over an HTTP request/response lifecycle.

It provides a unified API to access request data (headers, body, params), parse input into structs, and build responses.

This interface is designed to be framework-agnostic, allowing different HTTP engines (Fiber, Echo, net/http, etc.) to implement it.

type Converter

type Converter[T any] interface {
	// Convert transforms a generic Handler into a framework-specific handler.
	Convert(h Handler) T
}

Converter defines a generic adapter that transforms a Handler into a framework-specific handler type.

This abstraction allows the same business logic (Handler) to be reused across different HTTP frameworks.

T represents the target handler type (e.g., fiber.Handler, echo.HandlerFunc).

Example (Fiber):

type FiberConverter struct{}

func (f FiberConverter) Convert(h Handler) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // Wrap framework context into our abstraction
        ctx := config.FiberContext{Ctx: c}

        // Execute handler
        res := h(Atendi9Context{ctx})

        // Middleware flow control
        if res.GoNext() {
            return c.Next()
        }

        // File response
        if len(res.FilePath) > 0 {
            return c.SendFile(res.FilePath)
        }

        // Error handling (priority over Data)
        if err := res.Err; err != nil {
            return c.Status(res.Status()).JSON(fiber.Map{
                "err": err.Error(),
            })
        }

        // String response optimization
        if v, ok := res.Data.(string); ok {
            return c.Status(res.Status()).SendString(v)
        }

        // Default: JSON response
        return c.Status(res.Status()).JSON(res.Data)
    }
}

type Handler

type Handler func(c Context) Response

Handler represents a generic request handler.

It receives a Context abstraction and returns a Response, allowing full control over request parsing and response generation.

This design enables framework-independent business logic.

Example:

func HelloHandler(c Context) Response {
    name := c.Query("name", "guest")

    return Response{
        Data: map[string]string{
            "message": "Hello " + name,
        },
    }
}

Example with error:

func ErrorHandler(c Context) Response {
    return Response{
        Err: errors.New("something went wrong"),
        StatusCode: 500,
    }
}

type Response

type Response struct {
	// Err represents an error that occurred during request handling.
	//
	// If set, it usually takes priority over Data and will be
	// serialized as an error response by the converter.
	Err error

	// StatusCode defines the HTTP status code to be returned.
	//
	// If not explicitly set or invalid, it defaults to 200 (OK).
	StatusCode int

	// FilePath, if set, indicates that a file should be sent
	// as the response instead of JSON or raw data.
	FilePath string

	// Data holds the response payload.
	//
	// It can be:
	//   - struct/map → serialized as JSON
	//   - string     → sent as plain text (depending on converter)
	//   - any other type supported by the converter
	Data any
	// contains filtered or unexported fields
}

Response represents the result of a handler execution.

It encapsulates all possible outcomes of a request, including:

  • HTTP status code
  • response data (JSON, string, etc.)
  • file responses
  • error handling
  • middleware flow control (Next)

This struct is designed to be interpreted by a Converter, which translates it into a specific framework response (Fiber, Echo, etc.).

func SendStatus

func SendStatus(statusCode int) Response

SendStatus creates a Response with only a status code.

Useful for simple responses without a body.

Example:

return SendStatus(404)

func (Response) GoNext

func (r Response) GoNext() bool

GoNext returns whether the handler chain should continue.

This method should be used instead of accessing internal fields directly, ensuring proper encapsulation.

Example:

if res.GoNext() {
    // call next middleware
}

func (Response) JSON added in v1.0.1

func (r Response) JSON(data any) Response

JSON sets the JSON payload on the response.

The provided data is assigned to the Data field; all other fields already set on the receiver (StatusCode, Err, FilePath, next) are preserved. The StatusCode is normalized to a valid value via Status.

The provided data should be serializable by the converter (usually to JSON).

Example:

return Response{}.JSON(map[string]string{
    "message": "ok",
})

func (Response) Next

func (r Response) Next() Response

Next marks the response to pass execution to the next handler.

This is typically used in middleware scenarios.

Example:

return Response{}.Next()

func (Response) Status

func (r Response) Status() int

Status returns a valid HTTP status code.

If the StatusCode is unset (its zero value, 0) or falls outside the valid HTTP range [100, 599], it defaults to http.StatusOK (200).

Any explicitly set code within the valid range is returned as-is, so 1xx informational codes and an intentional 200 are both preserved.

Jump to

Keyboard shortcuts

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