Loading documentation...
Loading documentation...
Loading documentation...
Note: This is a developer-maintained documentation page. The content here is not auto-generated and should be updated manually to explain how to customize and extend Helix.
Helix is designed to be highly customizable while maintaining sensible defaults. You can customize server configuration, error handling, middleware, and more through the functional options pattern and extension points.
Helix uses the functional options pattern for configuration. This provides:
All server customization happens through Option functions passed to helix.New() or helix.Default():
s := helix.New(
helix.WithAddr(":3000"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithBasePath("/api/v1"),
helix.WithTLS("cert.pem", "key.pem"),
helix.WithErrorHandler(customErrorHandler),
helix.HideBanner(),
)helix.New() or helix.Default()Build() pre-compiles middleware chain (called automatically)Helix provides several extension points:
s := helix.New(
helix.WithAddr(":3000"), // Listen address
helix.WithReadTimeout(30 * time.Second), // Request read timeout
helix.WithWriteTimeout(30 * time.Second), // Response write timeout
helix.WithIdleTimeout(120 * time.Second), // Keep-alive timeout
helix.WithGracePeriod(30 * time.Second), // Shutdown grace period
helix.WithMaxHeaderBytes(1 << 20), // Max header size (1MB)
)// Simple TLS with certificate files
s := helix.New(
helix.WithTLS("cert.pem", "key.pem"),
)
// Advanced TLS configuration
s := helix.New(
helix.WithTLSConfig(&tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}),
)Set a prefix for all routes:
s := helix.New(
helix.WithBasePath("/api/v1"),
)
// Route "/users" becomes "/api/v1/users"
s.GET("/users", handler)Replace the default RFC 7807 error handler:
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Custom error handling logic
switch e := err.(type) {
case helix.Problem:
// Handle Problem errors
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
// Handle validation errors
helix.WriteValidationProblem(w, e)
default:
// Handle other errors
helix.InternalServerError(w, "internal server error")
}
}
s := helix.New(
helix.WithErrorHandler(customErrorHandler),
)Error handlers are injected as middleware, so they have access to the full request context:
func loggingErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Log error with request context
logs.ErrorContext(r.Context(), "handler error",
logs.Err(err),
logs.String("path", r.URL.Path),
logs.String("method", r.Method),
)
// Use default handler for actual response
helix.HandleError(w, r, err)
}Add middleware that applies to all routes:
s := helix.New()
// Add middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.Use(customMiddleware)
// Middleware executes in order addedApply middleware to specific routes or groups:
// Group with middleware
admin := s.Group("/admin", authMiddleware, adminOnlyMiddleware)
admin.GET("/stats", getStats)
// Resource with middleware
s.Resource("/users", authMiddleware).
List(listUsers).
Create(createUser)Create custom middleware that works with any http.Handler:
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
logs.Info("request completed",
logs.String("method", r.Method),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
logs.Duration("duration", duration),
)
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}Execute code when the server starts:
s.OnStart(func(s *helix.Server) {
logs.Info("server starting",
logs.String("addr", s.Addr()),
logs.String("version", helix.Version),
)
// Initialize services, connect to databases, etc.
db.Connect()
cache.Connect()
})Execute code during graceful shutdown:
s.OnStop(func(ctx context.Context, s *helix.Server) {
logs.Info("server shutting down")
// Cleanup resources
db.Close()
cache.Close()
// Wait for in-flight requests
// Context is already cancelled, so operations should respect it
<-ctx.Done()
})s := helix.New(helix.HideBanner())s := helix.New(
helix.WithCustomBanner(`
╔═══════════════════════════╗
║ My Awesome API v1.0.0 ║
╚═══════════════════════════╝
`),
)s := helix.Default(
helix.WithAddr(":8080"),
helix.HideBanner(), // Cleaner output in dev
)
// Development middleware
s.Use(middleware.Logger(middleware.LogFormatDev))
s.Use(middleware.CORSAllowAll()) // Allow all origins in devs := helix.New(
helix.WithAddr(":8080"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithTLS("cert.pem", "key.pem"),
)
// Production middleware bundle
for _, mw := range middleware.Production() {
s.Use(mw)
}
// Lifecycle hooks
s.OnStart(func(s *helix.Server) {
// Health checks, metrics, etc.
})
s.OnStop(func(ctx context.Context, s *helix.Server) {
// Graceful shutdown
})s := helix.New(
helix.WithBasePath("/api/v1"),
helix.WithErrorHandler(apiErrorHandler),
)
// API middleware bundle
for _, mw := range middleware.API() {
s.Use(mw)
}
// Routes are automatically prefixed
s.GET("/users", listUsers) // Becomes /api/v1/usersHelix uses functional options instead of a config struct because:
Error handlers are injected as middleware rather than stored in Server because:
Lifecycle hooks are separate from server options because:
Problem: Some options (like WithBasePath) must be set before registering routes.
Solution: Set all options during server creation:
// ❌ Wrong - base path set after routes
s := helix.New()
s.GET("/users", handler)
s.WithBasePath("/api") // Too late!
// ✅ Correct - set options first
s := helix.New(helix.WithBasePath("/api"))
s.GET("/users", handler) // Route is /api/usersProblem: Custom error handlers must handle all error types or use a default case.
Solution: Always include a default case:
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
helix.WriteProblem(w, helix.ErrInternal.WithDetailf("%v", err))
}
}Problem: Shutdown hooks receive a cancelled context. Operations must respect cancellation.
Solution: Always check context in shutdown hooks:
s.OnStop(func(ctx context.Context, s *helix.Server) {
// ❌ Wrong - doesn't respect cancellation
db.Close() // Blocks indefinitely
// ✅ Correct - respects context
done := make(chan struct{})
go func() {
db.Close()
close(done)
}()
select {
case <-done:
// Closed successfully
case <-ctx.Done():
// Timeout - force close
db.ForceClose()
}
})Customize service registration:
s := helix.New()
// Register global services
helix.Register(userService)
helix.Register(emailService)
// Provide request-scoped services via middleware
s.Use(helix.ProvideMiddleware(func(r *http.Request) *Transaction {
return db.BeginTx(r.Context())
}))Integrate with Helix's logging package:
import "github.com/kolosys/helix/logs"
s := helix.New()
// Configure logger
log := logs.New(
logs.WithLevel(logs.InfoLevel),
logs.WithFormatter(&logs.JSONFormatter{}),
logs.WithCaller(),
)
// Use in lifecycle hooks
s.OnStart(func(s *helix.Server) {
log.Info("server starting", logs.String("addr", s.Addr()))
})Add metrics middleware:
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
metrics.RecordRequest(r.Method, r.URL.Path, rw.status, duration)
})
}
s.Use(metricsMiddleware)This documentation should be updated by package maintainers to reflect the actual architecture and design patterns used.
Note: This is a developer-maintained documentation page. The content here is not auto-generated and should be updated manually to explain how to customize and extend Helix.
Helix is designed to be highly customizable while maintaining sensible defaults. You can customize server configuration, error handling, middleware, and more through the functional options pattern and extension points.
Helix uses the functional options pattern for configuration. This provides:
All server customization happens through Option functions passed to helix.New() or helix.Default():
s := helix.New(
helix.WithAddr(":3000"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithBasePath("/api/v1"),
helix.WithTLS("cert.pem", "key.pem"),
helix.WithErrorHandler(customErrorHandler),
helix.HideBanner(),
)helix.New() or helix.Default()Build() pre-compiles middleware chain (called automatically)Helix provides several extension points:
s := helix.New(
helix.WithAddr(":3000"), // Listen address
helix.WithReadTimeout(30 * time.Second), // Request read timeout
helix.WithWriteTimeout(30 * time.Second), // Response write timeout
helix.WithIdleTimeout(120 * time.Second), // Keep-alive timeout
helix.WithGracePeriod(30 * time.Second), // Shutdown grace period
helix.WithMaxHeaderBytes(1 << 20), // Max header size (1MB)
)// Simple TLS with certificate files
s := helix.New(
helix.WithTLS("cert.pem", "key.pem"),
)
// Advanced TLS configuration
s := helix.New(
helix.WithTLSConfig(&tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}),
)Set a prefix for all routes:
s := helix.New(
helix.WithBasePath("/api/v1"),
)
// Route "/users" becomes "/api/v1/users"
s.GET("/users", handler)Replace the default RFC 7807 error handler:
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Custom error handling logic
switch e := err.(type) {
case helix.Problem:
// Handle Problem errors
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
// Handle validation errors
helix.WriteValidationProblem(w, e)
default:
// Handle other errors
helix.InternalServerError(w, "internal server error")
}
}
s := helix.New(
helix.WithErrorHandler(customErrorHandler),
)Error handlers are injected as middleware, so they have access to the full request context:
func loggingErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Log error with request context
logs.ErrorContext(r.Context(), "handler error",
logs.Err(err),
logs.String("path", r.URL.Path),
logs.String("method", r.Method),
)
// Use default handler for actual response
helix.HandleError(w, r, err)
}Add middleware that applies to all routes:
s := helix.New()
// Add middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.Use(customMiddleware)
// Middleware executes in order addedApply middleware to specific routes or groups:
// Group with middleware
admin := s.Group("/admin", authMiddleware, adminOnlyMiddleware)
admin.GET("/stats", getStats)
// Resource with middleware
s.Resource("/users", authMiddleware).
List(listUsers).
Create(createUser)Create custom middleware that works with any http.Handler:
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
logs.Info("request completed",
logs.String("method", r.Method),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
logs.Duration("duration", duration),
)
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}Execute code when the server starts:
s.OnStart(func(s *helix.Server) {
logs.Info("server starting",
logs.String("addr", s.Addr()),
logs.String("version", helix.Version),
)
// Initialize services, connect to databases, etc.
db.Connect()
cache.Connect()
})Execute code during graceful shutdown:
s.OnStop(func(ctx context.Context, s *helix.Server) {
logs.Info("server shutting down")
// Cleanup resources
db.Close()
cache.Close()
// Wait for in-flight requests
// Context is already cancelled, so operations should respect it
<-ctx.Done()
})s := helix.New(helix.HideBanner())s := helix.New(
helix.WithCustomBanner(`
╔═══════════════════════════╗
║ My Awesome API v1.0.0 ║
╚═══════════════════════════╝
`),
)s := helix.Default(
helix.WithAddr(":8080"),
helix.HideBanner(), // Cleaner output in dev
)
// Development middleware
s.Use(middleware.Logger(middleware.LogFormatDev))
s.Use(middleware.CORSAllowAll()) // Allow all origins in devs := helix.New(
helix.WithAddr(":8080"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithTLS("cert.pem", "key.pem"),
)
// Production middleware bundle
for _, mw := range middleware.Production() {
s.Use(mw)
}
// Lifecycle hooks
s.OnStart(func(s *helix.Server) {
// Health checks, metrics, etc.
})
s.OnStop(func(ctx context.Context, s *helix.Server) {
// Graceful shutdown
})s := helix.New(
helix.WithBasePath("/api/v1"),
helix.WithErrorHandler(apiErrorHandler),
)
// API middleware bundle
for _, mw := range middleware.API() {
s.Use(mw)
}
// Routes are automatically prefixed
s.GET("/users", listUsers) // Becomes /api/v1/usersHelix uses functional options instead of a config struct because:
Error handlers are injected as middleware rather than stored in Server because:
Lifecycle hooks are separate from server options because:
Problem: Some options (like WithBasePath) must be set before registering routes.
Solution: Set all options during server creation:
// ❌ Wrong - base path set after routes
s := helix.New()
s.GET("/users", handler)
s.WithBasePath("/api") // Too late!
// ✅ Correct - set options first
s := helix.New(helix.WithBasePath("/api"))
s.GET("/users", handler) // Route is /api/usersProblem: Custom error handlers must handle all error types or use a default case.
Solution: Always include a default case:
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
helix.WriteProblem(w, helix.ErrInternal.WithDetailf("%v", err))
}
}Problem: Shutdown hooks receive a cancelled context. Operations must respect cancellation.
Solution: Always check context in shutdown hooks:
s.OnStop(func(ctx context.Context, s *helix.Server) {
// ❌ Wrong - doesn't respect cancellation
db.Close() // Blocks indefinitely
// ✅ Correct - respects context
done := make(chan struct{})
go func() {
db.Close()
close(done)
}()
select {
case <-done:
// Closed successfully
case <-ctx.Done():
// Timeout - force close
db.ForceClose()
}
})Customize service registration:
s := helix.New()
// Register global services
helix.Register(userService)
helix.Register(emailService)
// Provide request-scoped services via middleware
s.Use(helix.ProvideMiddleware(func(r *http.Request) *Transaction {
return db.BeginTx(r.Context())
}))Integrate with Helix's logging package:
import "github.com/kolosys/helix/logs"
s := helix.New()
// Configure logger
log := logs.New(
logs.WithLevel(logs.InfoLevel),
logs.WithFormatter(&logs.JSONFormatter{}),
logs.WithCaller(),
)
// Use in lifecycle hooks
s.OnStart(func(s *helix.Server) {
log.Info("server starting", logs.String("addr", s.Addr()))
})Add metrics middleware:
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
metrics.RecordRequest(r.Method, r.URL.Path, rw.status, duration)
})
}
s.Use(metricsMiddleware)This documentation should be updated by package maintainers to reflect the actual architecture and design patterns used.
s := helix.New(
helix.WithAddr(":3000"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithBasePath("/api/v1"),
helix.WithTLS("cert.pem", "key.pem"),
helix.WithErrorHandler(customErrorHandler),
helix.HideBanner(),
)s := helix.New(
helix.WithAddr(":3000"), // Listen address
helix.WithReadTimeout(30 * time.Second), // Request read timeout
helix.WithWriteTimeout(30 * time.Second), // Response write timeout
helix.WithIdleTimeout(120 * time.Second), // Keep-alive timeout
helix.WithGracePeriod(30 * time.Second), // Shutdown grace period
helix.WithMaxHeaderBytes(1 << 20), // Max header size (1MB)
)// Simple TLS with certificate files
s := helix.New(
helix.WithTLS("cert.pem", "key.pem"),
)
// Advanced TLS configuration
s := helix.New(
helix.WithTLSConfig(&tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}),
)s := helix.New(
helix.WithBasePath("/api/v1"),
)
// Route "/users" becomes "/api/v1/users"
s.GET("/users", handler)type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Custom error handling logic
switch e := err.(type) {
case helix.Problem:
// Handle Problem errors
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
// Handle validation errors
helix.WriteValidationProblem(w, e)
default:
// Handle other errors
helix.InternalServerError(w, "internal server error")
}
}
s := helix.New(
helix.WithErrorHandler(customErrorHandler),
)func loggingErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Log error with request context
logs.ErrorContext(r.Context(), "handler error",
logs.Err(err),
logs.String("path", r.URL.Path),
logs.String("method", r.Method),
)
// Use default handler for actual response
helix.HandleError(w, r, err)
}s := helix.New()
// Add middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.Use(customMiddleware)
// Middleware executes in order added// Group with middleware
admin := s.Group("/admin", authMiddleware, adminOnlyMiddleware)
admin.GET("/stats", getStats)
// Resource with middleware
s.Resource("/users", authMiddleware).
List(listUsers).
Create(createUser)func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
logs.Info("request completed",
logs.String("method", r.Method),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
logs.Duration("duration", duration),
)
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}s.OnStart(func(s *helix.Server) {
logs.Info("server starting",
logs.String("addr", s.Addr()),
logs.String("version", helix.Version),
)
// Initialize services, connect to databases, etc.
db.Connect()
cache.Connect()
})s.OnStop(func(ctx context.Context, s *helix.Server) {
logs.Info("server shutting down")
// Cleanup resources
db.Close()
cache.Close()
// Wait for in-flight requests
// Context is already cancelled, so operations should respect it
<-ctx.Done()
})s := helix.New(helix.HideBanner())s := helix.New(
helix.WithCustomBanner(`
╔═══════════════════════════╗
║ My Awesome API v1.0.0 ║
╚═══════════════════════════╝
`),
)s := helix.Default(
helix.WithAddr(":8080"),
helix.HideBanner(), // Cleaner output in dev
)
// Development middleware
s.Use(middleware.Logger(middleware.LogFormatDev))
s.Use(middleware.CORSAllowAll()) // Allow all origins in devs := helix.New(
helix.WithAddr(":8080"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithTLS("cert.pem", "key.pem"),
)
// Production middleware bundle
for _, mw := range middleware.Production() {
s.Use(mw)
}
// Lifecycle hooks
s.OnStart(func(s *helix.Server) {
// Health checks, metrics, etc.
})
s.OnStop(func(ctx context.Context, s *helix.Server) {
// Graceful shutdown
})s := helix.New(
helix.WithBasePath("/api/v1"),
helix.WithErrorHandler(apiErrorHandler),
)
// API middleware bundle
for _, mw := range middleware.API() {
s.Use(mw)
}
// Routes are automatically prefixed
s.GET("/users", listUsers) // Becomes /api/v1/users// ❌ Wrong - base path set after routes
s := helix.New()
s.GET("/users", handler)
s.WithBasePath("/api") // Too late!
// ✅ Correct - set options first
s := helix.New(helix.WithBasePath("/api"))
s.GET("/users", handler) // Route is /api/usersfunc errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
helix.WriteProblem(w, helix.ErrInternal.WithDetailf("%v", err))
}
}s.OnStop(func(ctx context.Context, s *helix.Server) {
// ❌ Wrong - doesn't respect cancellation
db.Close() // Blocks indefinitely
// ✅ Correct - respects context
done := make(chan struct{})
go func() {
db.Close()
close(done)
}()
select {
case <-done:
// Closed successfully
case <-ctx.Done():
// Timeout - force close
db.ForceClose()
}
})s := helix.New()
// Register global services
helix.Register(userService)
helix.Register(emailService)
// Provide request-scoped services via middleware
s.Use(helix.ProvideMiddleware(func(r *http.Request) *Transaction {
return db.BeginTx(r.Context())
}))import "github.com/kolosys/helix/logs"
s := helix.New()
// Configure logger
log := logs.New(
logs.WithLevel(logs.InfoLevel),
logs.WithFormatter(&logs.JSONFormatter{}),
logs.WithCaller(),
)
// Use in lifecycle hooks
s.OnStart(func(s *helix.Server) {
log.Info("server starting", logs.String("addr", s.Addr()))
})func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
metrics.RecordRequest(r.Method, r.URL.Path, rw.status, duration)
})
}
s.Use(metricsMiddleware)s := helix.New(
helix.WithAddr(":3000"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithBasePath("/api/v1"),
helix.WithTLS("cert.pem", "key.pem"),
helix.WithErrorHandler(customErrorHandler),
helix.HideBanner(),
)s := helix.New(
helix.WithAddr(":3000"), // Listen address
helix.WithReadTimeout(30 * time.Second), // Request read timeout
helix.WithWriteTimeout(30 * time.Second), // Response write timeout
helix.WithIdleTimeout(120 * time.Second), // Keep-alive timeout
helix.WithGracePeriod(30 * time.Second), // Shutdown grace period
helix.WithMaxHeaderBytes(1 << 20), // Max header size (1MB)
)// Simple TLS with certificate files
s := helix.New(
helix.WithTLS("cert.pem", "key.pem"),
)
// Advanced TLS configuration
s := helix.New(
helix.WithTLSConfig(&tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}),
)s := helix.New(
helix.WithBasePath("/api/v1"),
)
// Route "/users" becomes "/api/v1/users"
s.GET("/users", handler)type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Custom error handling logic
switch e := err.(type) {
case helix.Problem:
// Handle Problem errors
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
// Handle validation errors
helix.WriteValidationProblem(w, e)
default:
// Handle other errors
helix.InternalServerError(w, "internal server error")
}
}
s := helix.New(
helix.WithErrorHandler(customErrorHandler),
)func loggingErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
// Log error with request context
logs.ErrorContext(r.Context(), "handler error",
logs.Err(err),
logs.String("path", r.URL.Path),
logs.String("method", r.Method),
)
// Use default handler for actual response
helix.HandleError(w, r, err)
}s := helix.New()
// Add middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.Use(customMiddleware)
// Middleware executes in order added// Group with middleware
admin := s.Group("/admin", authMiddleware, adminOnlyMiddleware)
admin.GET("/stats", getStats)
// Resource with middleware
s.Resource("/users", authMiddleware).
List(listUsers).
Create(createUser)func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
logs.Info("request completed",
logs.String("method", r.Method),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
logs.Duration("duration", duration),
)
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}s.OnStart(func(s *helix.Server) {
logs.Info("server starting",
logs.String("addr", s.Addr()),
logs.String("version", helix.Version),
)
// Initialize services, connect to databases, etc.
db.Connect()
cache.Connect()
})s.OnStop(func(ctx context.Context, s *helix.Server) {
logs.Info("server shutting down")
// Cleanup resources
db.Close()
cache.Close()
// Wait for in-flight requests
// Context is already cancelled, so operations should respect it
<-ctx.Done()
})s := helix.New(helix.HideBanner())s := helix.New(
helix.WithCustomBanner(`
╔═══════════════════════════╗
║ My Awesome API v1.0.0 ║
╚═══════════════════════════╝
`),
)s := helix.Default(
helix.WithAddr(":8080"),
helix.HideBanner(), // Cleaner output in dev
)
// Development middleware
s.Use(middleware.Logger(middleware.LogFormatDev))
s.Use(middleware.CORSAllowAll()) // Allow all origins in devs := helix.New(
helix.WithAddr(":8080"),
helix.WithReadTimeout(30 * time.Second),
helix.WithWriteTimeout(30 * time.Second),
helix.WithIdleTimeout(120 * time.Second),
helix.WithGracePeriod(30 * time.Second),
helix.WithTLS("cert.pem", "key.pem"),
)
// Production middleware bundle
for _, mw := range middleware.Production() {
s.Use(mw)
}
// Lifecycle hooks
s.OnStart(func(s *helix.Server) {
// Health checks, metrics, etc.
})
s.OnStop(func(ctx context.Context, s *helix.Server) {
// Graceful shutdown
})s := helix.New(
helix.WithBasePath("/api/v1"),
helix.WithErrorHandler(apiErrorHandler),
)
// API middleware bundle
for _, mw := range middleware.API() {
s.Use(mw)
}
// Routes are automatically prefixed
s.GET("/users", listUsers) // Becomes /api/v1/users// ❌ Wrong - base path set after routes
s := helix.New()
s.GET("/users", handler)
s.WithBasePath("/api") // Too late!
// ✅ Correct - set options first
s := helix.New(helix.WithBasePath("/api"))
s.GET("/users", handler) // Route is /api/usersfunc errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
helix.WriteProblem(w, helix.ErrInternal.WithDetailf("%v", err))
}
}s.OnStop(func(ctx context.Context, s *helix.Server) {
// ❌ Wrong - doesn't respect cancellation
db.Close() // Blocks indefinitely
// ✅ Correct - respects context
done := make(chan struct{})
go func() {
db.Close()
close(done)
}()
select {
case <-done:
// Closed successfully
case <-ctx.Done():
// Timeout - force close
db.ForceClose()
}
})s := helix.New()
// Register global services
helix.Register(userService)
helix.Register(emailService)
// Provide request-scoped services via middleware
s.Use(helix.ProvideMiddleware(func(r *http.Request) *Transaction {
return db.BeginTx(r.Context())
}))import "github.com/kolosys/helix/logs"
s := helix.New()
// Configure logger
log := logs.New(
logs.WithLevel(logs.InfoLevel),
logs.WithFormatter(&logs.JSONFormatter{}),
logs.WithCaller(),
)
// Use in lifecycle hooks
s.OnStart(func(s *helix.Server) {
log.Info("server starting", logs.String("addr", s.Addr()))
})func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
duration := time.Since(start)
metrics.RecordRequest(r.Method, r.URL.Path, rw.status, duration)
})
}
s.Use(metricsMiddleware)