__ __ ___
/ // /__ / (_)_ __
/ _ / -_) / /\ \ /
/_//_/\__/_/_//_\_\
Developer friendly HTTP framework
Helix is a zero-dependency, high-performance HTTP web framework for Go with a focus on developer experience, type safety, and stdlib compatibility. Built by Kolosys for enterprise-grade applications.
- Zero Dependencies - Built entirely on Go's standard library
- High Performance - Zero-allocation hot paths using
sync.Pool - Type-Safe Handlers - Generic handlers with automatic request binding and response encoding
- RFC 7807 Problem Details - Standardized error responses out of the box
- Modular Architecture - First-class support for organizing routes into modules
- Fluent API - Chainable context methods for clean handler code
- Middleware Ecosystem - Comprehensive built-in middleware suite
- Dependency Injection - Type-safe service registry with request-scoped support
- Health Checks - Built-in Kubernetes-ready liveness and readiness probes
- Structured Logging - High-performance logging with JSON and text formatters
- Graceful Shutdown - Context-aware shutdown with configurable grace period
- stdlib Compatible - Works with any
http.Handlermiddleware
go get github.com/kolosys/helixRequires Go 1.24 or later.
The recommended way to build handlers in Helix is using HandleCtx, which provides a fluent API with automatic error handling:
package main
import "github.com/kolosys/helix"
func main() {
s := helix.Default(nil)
s.GET("/", helix.HandleCtx(func(c *helix.Ctx) error {
return c.OK(map[string]string{"message": "Hello, World!"})
}))
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id, err := c.ParamInt("id")
if err != nil {
return c.BadRequest("invalid user ID")
}
return c.OK(User{ID: id, Name: "John Doe"})
}))
s.POST("/users", helix.HandleCtx(func(c *helix.Ctx) error {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
return c.BadRequest("invalid request body")
}
user := User{ID: 1, Name: req.Name}
return c.Created(user)
}))
s.Start(":8080")
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
type CreateUserRequest struct {
Name string `json:"name"`
}s := helix.New(nil)Includes RequestID, Logger (dev format), and Recover middleware:
s := helix.Default(nil)s := helix.New(&helix.Options{
Addr: ":3000",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
GracePeriod: 30 * time.Second,
BasePath: "/api/v1",
TLSCertFile: "cert.pem",
TLSKeyFile: "key.pem",
ErrorHandler: customErrorHandler,
HideBanner: true,
})s.GET("/users", listUsers)
s.POST("/users", createUser)
s.PUT("/users/{id}", updateUser)
s.PATCH("/users/{id}", patchUser)
s.DELETE("/users/{id}", deleteUser)
s.HEAD("/users", headUsers)
s.OPTIONS("/users", optionsUsers)
s.Any("/echo", echoHandler) // All methods// Single parameter
s.GET("/users/{id}", handler) // Param(r, "id")
// Multiple parameters
s.GET("/users/{userId}/posts/{postId}", handler)
// Catch-all parameter
s.GET("/files/{path...}", handler) // Matches /files/a/b/cs.Static("/assets/", "./public")Helix supports three handler patterns. Choose based on your needs:
The HandleCtx pattern is recommended for most applications. It provides a fluent API with automatic error conversion to RFC 7807 responses:
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
// Access path params
id, err := c.ParamInt("id")
if err != nil {
return c.BadRequest("invalid user ID")
}
// Access query params
page := c.QueryInt("page", 1)
search := c.QueryDefault("q", "")
// Access headers
auth := c.Header("Authorization")
// Bind JSON body
var input CreateUserInput
if err := c.Bind(&input); err != nil {
return c.BadRequest("invalid input")
}
// Return response
return c.OK(users)
}))Use typed handlers when you want automatic request binding and compile-time type safety:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
s.POST("/users", helix.Handle(func(ctx context.Context, req CreateUserRequest) (User, error) {
// req is automatically bound from JSON body
user, err := userService.Create(ctx, req.Name, req.Email)
if err != nil {
return User{}, err
}
return user, nil // Automatically encoded as JSON
}))Handler Variants:
helix.HandleCreated(handler) // Returns 201 Created
helix.HandleAccepted(handler) // Returns 202 Accepted
helix.HandleWithStatus(201, h) // Custom status code
helix.HandleNoRequest(handler) // No request body (GET)
helix.HandleNoResponse(handler) // No response body (DELETE)
helix.HandleEmpty(handler) // No request, no responseUse standard handlers for stdlib compatibility and maximum control:
s.GET("/", func(w http.ResponseWriter, r *http.Request) {
helix.OK(w, map[string]string{"status": "ok"})
})See Handler Patterns for detailed guidance on when to use each pattern.
Bind request data to structs using struct tags:
type UpdateUserRequest struct {
// From path parameters
ID int `path:"id"`
// From query parameters
Include string `query:"include"`
// From headers
APIKey string `header:"X-API-Key"`
// From JSON body
Name string `json:"name"`
Email string `json:"email"`
// From form data
Avatar string `form:"avatar"`
}// Bind all sources
user, err := helix.Bind[UpdateUserRequest](r)
// Bind specific sources
query, err := helix.BindQuery[QueryParams](r)
path, err := helix.BindPath[PathParams](r)
headers, err := helix.BindHeader[HeaderParams](r)
body, err := helix.BindJSON[CreateRequest](r)
// Bind and validate
user, err := helix.BindAndValidate[CreateUserRequest](r)// Path parameters
id := helix.Param(r, "id")
userID, err := helix.ParamInt(r, "id")
uuid, err := helix.ParamUUID(r, "id")
// Query parameters
name := helix.Query(r, "name")
name := helix.QueryDefault(r, "name", "default")
page := helix.QueryInt(r, "page", 1)
active := helix.QueryBool(r, "active")
tags := helix.QuerySlice(r, "tags")
price := helix.QueryFloat64(r, "price", 0.0)helix.JSON(w, http.StatusOK, data)
helix.JSONPretty(w, http.StatusOK, data, " ")
helix.OK(w, data) // 200
helix.Created(w, data) // 201
helix.Accepted(w, data) // 202
helix.NoContent(w) // 204helix.Text(w, http.StatusOK, "Hello, World!")
helix.HTML(w, http.StatusOK, "<h1>Hello</h1>")
helix.Blob(w, http.StatusOK, "image/png", imageData)
helix.Stream(w, "application/octet-stream", reader)
helix.File(w, r, "/path/to/file")helix.Error(w, http.StatusBadRequest, "invalid input")
helix.BadRequest(w, "invalid input")
helix.Unauthorized(w, "authentication required")
helix.Forbidden(w, "access denied")
helix.NotFound(w, "user not found")
helix.InternalServerError(w, "something went wrong")helix.Attachment(w, "report.pdf") // Force download
helix.Inline(w, "image.png") // Display inline
helix.Redirect(w, r, "/new-url", http.StatusFound)Helix uses RFC 7807 Problem Details for standardized error responses:
// Return a problem from a handler
return helix.ErrNotFound.WithDetail("user 123 not found")
// Or create custom problems
return helix.NewProblem(
http.StatusConflict,
"duplicate_email",
"Email Already Exists",
).WithDetail("The email address is already registered")helix.ErrBadRequest // 400
helix.ErrUnauthorized // 401
helix.ErrForbidden // 403
helix.ErrNotFound // 404
helix.ErrMethodNotAllowed // 405
helix.ErrConflict // 409
helix.ErrGone // 410
helix.ErrUnprocessableEntity // 422
helix.ErrTooManyRequests // 429
helix.ErrInternal // 500
helix.ErrNotImplemented // 501
helix.ErrBadGateway // 502
helix.ErrServiceUnavailable // 503
helix.ErrGatewayTimeout // 504return helix.NotFoundf("user %d not found", id)
return helix.BadRequestf("invalid email: %s", email)
return helix.Conflictf("username %q already taken", username){
"type": "about:blank#not_found",
"title": "Not Found",
"status": 404,
"detail": "user 123 not found",
"instance": "/users/123"
}Implement the Validatable interface for automatic validation:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func (r *CreateUserRequest) Validate() error {
v := helix.NewValidationErrors()
if r.Name == "" {
v.Add("name", "name is required")
}
if r.Email == "" {
v.Add("email", "email is required")
} else if !strings.Contains(r.Email, "@") {
v.Add("email", "invalid email format")
}
if r.Age < 0 || r.Age > 150 {
v.Addf("age", "age must be between 0 and 150, got %d", r.Age)
}
return v.Err() // Returns nil if no errors
}Validation errors are returned as RFC 7807 with field-level details:
{
"type": "about:blank#unprocessable_entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "One or more validation errors occurred",
"instance": "/users",
"errors": [
{ "field": "name", "message": "name is required" },
{ "field": "email", "message": "invalid email format" }
]
}// Global middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatDev))
s.Use(middleware.Recover())
// Works with any func(http.Handler) http.Handler
s.Use(thirdPartyMiddleware)middleware.RequestID() // Generates X-Request-ID headermiddleware.Logger(middleware.LogFormatDev) // Colorized development
middleware.Logger(middleware.LogFormatJSON) // JSON format
middleware.Logger(middleware.LogFormatCombined) // Apache combined
middleware.Logger(middleware.LogFormatCommon) // Apache common
middleware.Logger(middleware.LogFormatShort) // Short format
middleware.Logger(middleware.LogFormatTiny) // Minimal format
// Custom format with tokens
middleware.LoggerWithFormat(":method :url :status :response-time")
// Advanced configuration
middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: middleware.LogFormatJSON,
Output: os.Stdout,
Skip: func(r *http.Request) bool { return r.URL.Path == "/health" },
TimeFormat: time.RFC3339,
Fields: map[string]string{"api_version": "header:X-API-Version"},
})middleware.Recover() // Recovers from panics, returns 500middleware.CORS() // Default permissive config
middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Authorization", "Content-Type"},
ExposeHeaders: []string{"X-Total-Count"},
AllowCredentials: true,
MaxAge: 86400,
})
middleware.CORSAllowAll() // Allow everything (dev only)middleware.RateLimit(100, 10) // 100 req/sec, burst of 10
middleware.RateLimitWithConfig(middleware.RateLimitConfig{
Rate: 100,
Burst: 10,
KeyFunc: func(r *http.Request) string { return r.Header.Get("X-API-Key") },
Handler: customRateLimitHandler,
SkipFunc: func(r *http.Request) bool { return r.URL.Path == "/health" },
})middleware.BasicAuth(map[string]string{
"admin": "secret",
"user": "password",
})middleware.Compress() // Gzip compressionmiddleware.Timeout(30 * time.Second)middleware.ETag() // Automatic ETag generationmiddleware.Cache(time.Hour) // HTTP cache headersPre-configured middleware sets for common scenarios:
// API server (RequestID, Logger JSON, Recover, CORS)
for _, mw := range middleware.API() {
s.Use(mw)
}
// Web application (RequestID, Logger Dev, Recover, Compress)
for _, mw := range middleware.Web() {
s.Use(mw)
}
// Production (RequestID, Logger Combined, Recover)
for _, mw := range middleware.Production() {
s.Use(mw)
}
// Development (same as helix.Default())
for _, mw := range middleware.Development() {
s.Use(mw)
}
// Secure (RequestID, Logger JSON, Recover, RateLimit)
for _, mw := range middleware.Secure(100, 10) {
s.Use(mw)
}
// Minimal (Recover only)
for _, mw := range middleware.Minimal() {
s.Use(mw)
}chain := middleware.Chain(
middleware.RequestID(),
middleware.Logger(middleware.LogFormatDev),
middleware.Recover(),
)
s.Use(chain)Organize routes with shared prefixes and middleware:
// Create a group
api := s.Group("/api/v1")
api.GET("/users", listUsers)
api.POST("/users", createUser)
// Group with middleware
admin := s.Group("/admin", authMiddleware, adminOnlyMiddleware)
admin.GET("/stats", getStats)
admin.DELETE("/users/{id}", deleteUser)
// Nested groups
v2 := api.Group("/v2")
v2.GET("/users", listUsersV2)
// Add middleware to group after creation
api.Use(rateLimitMiddleware)Modules provide a clean way to organize routes into separate files or packages:
// Define a module
type UserModule struct {
service *UserService
}
func (m *UserModule) Register(r helix.RouteRegistrar) {
r.GET("/", m.list)
r.POST("/", m.create)
r.GET("/{id}", m.get)
r.PUT("/{id}", m.update)
r.DELETE("/{id}", m.delete)
}
// Mount the module
s.Mount("/users", &UserModule{service: userService})
// Mount with middleware
s.Mount("/users", &UserModule{}, authMiddleware)
// Mount using a function
s.MountFunc("/posts", func(r helix.RouteRegistrar) {
r.GET("/", listPosts)
r.POST("/", createPost)
})
// Mount within a group
api := s.Group("/api/v1")
api.Mount("/users", &UserModule{})REST resource builder for CRUD operations:
// Fluent resource definition
s.Resource("/users").
List(listUsers). // GET /users
Create(createUser). // POST /users
Get(getUser). // GET /users/{id}
Update(updateUser). // PUT /users/{id}
Patch(patchUser). // PATCH /users/{id}
Delete(deleteUser) // DELETE /users/{id}
// All CRUD in one call
s.Resource("/posts").CRUD(listPosts, createPost, getPost, updatePost, deletePost)
// Read-only resource
s.Resource("/articles").ReadOnly(listArticles, getArticle)
// Custom actions
s.Resource("/users").
Get(getUser).
Custom("POST", "/{id}/activate", activateUser).
Custom("POST", "/{id}/deactivate", deactivateUser)
// Resource with middleware
s.Resource("/admin/users", authMiddleware, adminMiddleware).
CRUD(list, create, get, update, delete)
// Typed resources
helix.TypedResource[User](s, "/users").
List(listHandler).
Create(createHandler).
Get(getHandler).
Update(updateHandler).
Delete(deleteHandler)Type-safe service registry with global and request-scoped support:
// Register services at startup
userService := NewUserService(db)
emailService := NewEmailService(smtp)
helix.Register(userService)
helix.Register(emailService)
// Access in handlers
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
svc := helix.MustGet[*UserService]()
users, err := svc.List(c.Context())
if err != nil {
return err
}
return c.OK(users)
}))
// Safe access (returns ok bool)
svc, ok := helix.Get[*UserService]()// Add service to request context
ctx := helix.WithService(r.Context(), txn)
// Retrieve from context (falls back to global)
svc, ok := helix.FromContext[*Transaction](ctx)
svc := helix.MustFromContext[*Transaction](ctx)
// Middleware that provides request-scoped services
s.Use(helix.ProvideMiddleware(func(r *http.Request) *Transaction {
return db.BeginTx(r.Context())
}))Built-in pagination helpers:
type ListUsersRequest struct {
helix.Pagination // Embeds Page, Limit, Sort, Order, Cursor
Status string `query:"status"`
}
s.GET("/users", helix.Handle(func(ctx context.Context, req ListUsersRequest) (helix.PaginatedResponse[User], error) {
page := req.GetPage() // Default: 1
limit := req.GetLimit(20, 100) // Default 20, max 100
offset := req.GetOffset(limit) // Calculate SQL offset
sort := req.GetSort("created_at", []string{"created_at", "name"})
order := req.GetOrder() // "asc" or "desc"
users, total, err := userService.List(ctx, limit, offset, sort, order)
if err != nil {
return helix.PaginatedResponse[User]{}, err
}
return helix.NewPaginatedResponse(users, total, page, limit), nil
}))s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
p := c.BindPagination(20, 100) // defaultLimit, maxLimit
users, total, err := userService.List(c.Context(), p.GetPage(), p.GetLimit(20, 100))
if err != nil {
return err
}
return c.Paginated(users, total, p.GetPage(), p.GetLimit(20, 100))
})){
"items": [...],
"total": 150,
"page": 2,
"limit": 20,
"total_pages": 8,
"has_more": true
}Kubernetes-ready health check endpoints:
health := helix.Health().
Version("1.0.0").
Timeout(5 * time.Second).
CheckFunc("database", func(ctx context.Context) error {
return db.PingContext(ctx)
}).
CheckFunc("redis", func(ctx context.Context) error {
return redis.Ping(ctx).Err()
}).
Check("external_api", func(ctx context.Context) helix.HealthCheckResult {
start := time.Now()
err := callExternalAPI(ctx)
return helix.HealthCheckResult{
Status: helix.HealthStatusUp,
Latency: time.Since(start),
Details: map[string]any{"endpoint": "api.example.com"},
}
})
s.GET("/health", health.Handler())// Liveness probe (is the process running?)
s.GET("/health/live", helix.LivenessHandler())
// Readiness probe (is the service ready to accept traffic?)
s.GET("/health/ready", helix.ReadinessHandler(
func(ctx context.Context) error { return db.PingContext(ctx) },
func(ctx context.Context) error { return cache.Ping(ctx) },
)){
"status": "up",
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0.0",
"components": {
"database": {
"status": "up",
"latency_ms": 2
},
"redis": {
"status": "up",
"latency_ms": 1
}
}
}Helix includes flexible request logging middleware with Morgan.js-style formats:
import "github.com/kolosys/helix/middleware"
// Default dev format
s := helix.Default(nil) // Includes Logger middleware
// Custom format
s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Output: middleware.TextOutput(os.Stdout, middleware.LogFormatCombined),
}))
// JSON output
s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Output: middleware.TextOutputWithOptions(os.Stdout, middleware.LogFormatJSON, middleware.TextOutputOptions{
JSONPretty: true,
}),
}))
// Custom format string
s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Output: middleware.TextOutputCustom(os.Stdout, ":method :path :status :latency"),
}))middleware.LogFormatCombined // Apache combined format
middleware.LogFormatCommon // Apache common format
middleware.LogFormatDev // Concise colored output (default)
middleware.LogFormatShort // Shorter than combined
middleware.LogFormatTiny // Minimal output
middleware.LogFormatJSON // JSON formatFor application logging, use the standard library:
import "log"
log.Printf("Server starting on %s", s.Addr())
log.Println("Request processed")s.OnStart(func(s *helix.Server) {
log.Printf("Server starting on %s", s.Addr())
})
s.OnStop(func(ctx context.Context, s *helix.Server) {
log.Println("Server shutting down...")
db.Close()
})// Get all registered routes
routes := s.Routes()
for _, r := range routes {
fmt.Printf("%s %s\n", r.Method, r.Pattern)
}
// Print routes to writer
s.PrintRoutes(os.Stdout)Configure the server using the Options struct:
| Field | Type | Description | Default |
|---|---|---|---|
Addr |
string |
Server listen address | :8080 |
ReadTimeout |
time.Duration |
Maximum duration for reading request | 30s |
WriteTimeout |
time.Duration |
Maximum duration for writing response | 30s |
IdleTimeout |
time.Duration |
Maximum time to wait for next request | 120s |
GracePeriod |
time.Duration |
Shutdown grace period | 30s |
MaxHeaderBytes |
int |
Maximum size of request headers | 0 (none) |
BasePath |
string |
Base path prefix for all routes | "" |
TLSCertFile |
string |
Path to TLS certificate file | "" |
TLSKeyFile |
string |
Path to TLS key file | "" |
TLSConfig |
*tls.Config |
Custom TLS configuration | nil |
ErrorHandler |
ErrorHandler |
Custom error handler | RFC 7807 |
HideBanner |
bool |
Hide startup banner | false |
Banner |
string |
Custom startup banner | Default |
See the examples directory for complete working examples:
- basic - Simple handlers and routing
- crud - CRUD operations with typed handlers
- groups - Route grouping and nested groups
- middleware - Middleware usage patterns
- modular - Modular architecture with DI
- resource - REST resource builder
- validation - Request validation
Helix is designed for high performance:
- Zero allocations in hot paths using
sync.Poolfor contexts and parameters - Pre-compiled middleware chains via
s.Build() - Radix tree router for efficient route matching
- Buffer pooling for JSON encoding
- Minimal reflection - binding info is cached
// Call Build() after registering all routes and middleware
s.Build()
// Then start the server
s.Start(":8080")Contributions are welcome! Please feel free to submit a Pull Request.
Helix is released under the MIT License.
Built with ❤️ by Kolosys