Build powerful Go web APIs with annotations and zero boilerplate
Axon is a modern, annotation-driven web framework for Go that eliminates boilerplate through intelligent code generation. It seamlessly integrates Uber FX for dependency injection and Echo for high-performance HTTP routing, letting you focus on business logic instead of wiring.
- Annotation-Driven: Define controllers, routes, and middleware with simple comments
- Zero Boilerplate: Automatic code generation for DI, routing, and middleware chains
- High Performance: Built on Echo with optimized route registration and middleware application
- Type-Safe: Full type safety for parameters, responses, and dependency injection
- Modular: Clean separation with auto-generated FX modules
- Developer-Friendly: Hot reload support and comprehensive error reporting
go install github.com/toyz/axon/cmd/axon@latest//axon::controller -Prefix=/api/v1/users -Middleware=AuthMiddleware -Priority=10
type UserController struct {
//axon::inject
UserService *services.UserService
}
//axon::route GET /search -Priority=10
func (c *UserController) SearchUsers(ctx echo.Context, query axon.QueryMap) ([]*User, error) {
name := query.Get("name")
return c.UserService.SearchUsers(name)
}
//axon::route GET /{id:int} -Priority=50
func (c *UserController) GetUser(id int) (*User, error) {
return c.UserService.GetUser(id)
}# Generate all the magic
axon ./internal/...
# Use in your main.go
fx.New(
controllers.AutogenModule,
services.AutogenModule,
middleware.AutogenModule,
).Run()That's it! Axon handles routing, middleware, dependency injection, and parameter parsing automatically.
Controllers are the heart of your API. Axon automatically generates route handlers, applies middleware, and manages dependencies.
//axon::controller -Prefix=/api/v1/products -Middleware=AuthMiddleware -Priority=20
type ProductController struct {
//axon::inject
ProductService *services.ProductService
//axon::inject
Logger *slog.Logger
}
//axon::route GET / -Priority=10
func (c *ProductController) ListProducts(ctx echo.Context, query axon.QueryMap) ([]*Product, error) {
limit := query.GetIntDefault("limit", 10)
return c.ProductService.List(limit)
}
//axon::route GET /{id:uuid.UUID} -Priority=50
func (c *ProductController) GetProduct(id uuid.UUID) (*Product, error) {
return c.ProductService.GetByID(id)
}What Axon generates:
- Echo route registration with proper middleware chains
- Type-safe parameter extraction and validation
- Automatic JSON serialization/deserialization
- FX dependency injection providers
- Route priority ordering (lower numbers = higher priority)
Define middleware once, apply everywhere with perfect ordering control.
//axon::middleware AuthMiddleware
type AuthMiddleware struct {
//axon::inject
JWTService *services.JWTService
}
func (m *AuthMiddleware) Handle(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if !m.JWTService.ValidateToken(token) {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
}
return next(c)
}
}
//axon::middleware LoggingMiddleware -Global -Priority=50
type LoggingMiddleware struct {
//axon::inject
Logger *slog.Logger
}Apply middleware at any level:
- Global:
//axon::middleware -Global -Priority=1(Priority only works with -Global) - Controller:
//axon::controller -Middleware=AuthMiddleware - Route:
//axon::route GET /admin -Middleware=AdminMiddleware
Important Middleware Restrictions:
- Priority: Only works when combined with
-Globalflag - Routes: Middleware do not support the
-Routesparameter (use controller/route level instead)
Build robust services with automatic dependency injection and lifecycle hooks.
//axon::service -Init
type DatabaseService struct {
//axon::inject
Config *config.Config
connected bool
}
func (s *DatabaseService) Start(ctx context.Context) error {
fmt.Printf("Connecting to database: %s\n", s.Config.DatabaseURL)
s.connected = true
return nil
}
func (s *DatabaseService) Stop(ctx context.Context) error {
s.connected = false
return nil
}
//axon::service -Init=Background -Mode=Transient
type BackgroundWorker struct {
//axon::inject
DatabaseService *DatabaseService
}
func (s *BackgroundWorker) Start(ctx context.Context) error {
// This will run in its own goroutine
go s.processJobs()
return nil
}
// Injected as: func() *BackgroundWorker (new instance per request)Control the exact order of controllers, routes, and middleware with priorities:
// Routes with priorities (lower = registered first)
//axon::route GET /users/profile -Priority=10 // Matches before /{id}
//axon::route GET /users/admin -Priority=20 // Matches before /{id}
//axon::route GET /users/{id:int} -Priority=50 // Catch-all for IDs
// Controllers with priorities
//axon::controller -Priority=10 // API controllers first
//axon::controller -Priority=999 // Catch-all controllers last
// Global middleware with priorities (Priority only works with -Global)
//axon::middleware SecurityMiddleware -Global -Priority=1 // Security first
//axon::middleware LoggingMiddleware -Global -Priority=50 // Logging laterNo more manual parameter parsing or type conversion errors:
//axon::route GET /search
func (c *Controller) Search(ctx echo.Context, query axon.QueryMap) (*SearchResult, error) {
// All type-safe with automatic defaults
term := query.Get("q") // string
page := query.GetIntDefault("page", 1) // int, defaults to 1
limit := query.GetIntDefault("limit", 10) // int, defaults to 10
active := query.GetBool("active") // bool, defaults to false
price := query.GetFloat64("max_price") // float64, defaults to 0.0
return c.SearchService.Search(term, page, limit, active, price)
}Return data in the most natural way for your use case:
// Simple data + error (most common)
func (c *Controller) GetUser(id int) (*User, error) {
return c.UserService.GetUser(id) // Auto JSON + status codes
}
// Custom responses with full control
func (c *Controller) CreateUser(user User) (*axon.Response, error) {
created, err := c.UserService.Create(user)
if err != nil {
return nil, err
}
return axon.Created(created).
WithHeader("Location", fmt.Sprintf("/users/%d", created.ID)).
WithSecureCookie("session", sessionID, "/", 3600), nil
}
// Error-only for operations
func (c *Controller) DeleteUser(id int) error {
return c.UserService.Delete(id) // Auto 204 No Content
}Extend Axon with your own parameter types:
//axon::route_parser ProductCode
func ParseProductCode(c echo.Context, value string) (ProductCode, error) {
if !strings.HasPrefix(value, "PROD-") {
return "", fmt.Errorf("invalid product code format")
}
return ProductCode(value), nil
}
// Use in routes
//axon::route GET /products/{code:ProductCode}
func (c *Controller) GetByCode(code ProductCode) (*Product, error) {
return c.ProductService.GetByCode(string(code))
}Transform structs into powerful HTTP controllers.
Flags:
-Prefix=/path- URL prefix for all routes (creates Echo groups)-Middleware=Name1,Name2- Apply middleware to all routes-Priority=N- Registration order (lower = first, default: 100)
//axon::controller -Prefix=/api/v1/users -Middleware=AuthMiddleware -Priority=10
type UserController struct {
//axon::inject
UserService *services.UserService
}Define HTTP route handlers with automatic parameter binding.
Flags:
-Middleware=Name1,Name2- Route-specific middleware-Priority=N- Route registration order (lower = first, default: 100)-PassContext- Injectecho.Contextas first parameter
//axon::route GET /search -Priority=10 -Middleware=LoggingMiddleware
func (c *Controller) SearchUsers(ctx echo.Context, query axon.QueryMap) ([]*User, error) {}
//axon::route GET /{id:int} -Priority=50
func (c *Controller) GetUser(id int) (*User, error) {}
//axon::route POST / -PassContext -Middleware=ValidationMiddleware
func (c *Controller) CreateUser(ctx echo.Context, user User) (*axon.Response, error) {}Create reusable middleware components.
Flags:
-Priority=N- Execution order for global middleware (lower = first, only works with -Global)-Global- Apply to all routes automatically
Note: Middleware do not support -Routes parameter. Use controller or route-level middleware instead.
//axon::middleware AuthMiddleware
type AuthMiddleware struct {
//axon::inject
JWTService *services.JWTService
}
func (m *AuthMiddleware) Handle(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Middleware logic here
return next(c)
}
}
// Global middleware with priority ordering
//axon::middleware SecurityMiddleware -Global -Priority=1
type SecurityMiddleware struct {}
//axon::middleware LoggingMiddleware -Global -Priority=50
type LoggingMiddleware struct {}Define business services with lifecycle management.
Note:
//axon::coreis deprecated. Use//axon::serviceinstead.
Flags:
-Init[=Same|Background]- Enable lifecycle hooks with execution modeSame: Start/Stop runs on the same thread (blocking) - default if no value specifiedBackground: Start/Stop runs in its own goroutine (non-blocking)
-Mode=Singleton|Transient- Instance lifecycle (default: Singleton)-Constructor=FunctionName- Use custom constructor function instead of generated one-Manual=ModuleName- Reference existing FX module
//axon::service -Init
type DatabaseService struct {
//axon::inject
Config *config.Config
connected bool
}
//axon::service -Init=Background
type CrawlerService struct {
// Background service for async operations
}
//axon::service -Mode=Transient
type SessionService struct {
//axon::inject
DatabaseService *DatabaseService
sessionID string
}
//axon::service -Constructor=NewCustomDatabaseService
type DatabaseService struct {
Config *config.Config
db *sql.DB
}
// Custom constructor function - handles all initialization
func NewCustomDatabaseService(config *config.Config) (*DatabaseService, error) {
db, err := sql.Open("postgres", config.DatabaseURL)
if err != nil {
return nil, err
}
return &DatabaseService{
Config: config,
db: db,
}, nil
}
func (s *DatabaseService) Start(ctx context.Context) error {
// Runs on same thread (blocking)
var err error
s.db, err = sql.Open("postgres", s.Config.DatabaseURL)
return err
}
func (s *DatabaseService) Stop(ctx context.Context) error {
return s.db.Close()
}
//axon::service -Init=Background -Mode=Singleton
type BackgroundWorker struct {
//axon::inject
DatabaseService *DatabaseService
}
func (s *BackgroundWorker) Start(ctx context.Context) error {
// Runs in its own goroutine (non-blocking)
s.processJobs()
return nil
}Mark fields for dependency injection.
Mark fields for initialization (not injection).
type UserService struct {
//axon::inject
DatabaseService *DatabaseService // Injected dependency
//axon::inject
Logger *slog.Logger // Injected dependency
//axon::init
cache map[string]*User // Initialized field
//axon::init
mutex sync.RWMutex // Initialized field
}# Generate code for all packages
axon ./internal/...
# Generate specific packages
axon ./internal/controllers ./internal/services
# Clean generated files
axon --clean ./...
# Verbose output for debugging
axon --verbose ./internal/...
# Custom module name
axon -module=github.com/your-org/app ./internal/...your-app/
├── cmd/
│ └── server/
│ └── main.go # Application entry point
├── internal/
│ ├── controllers/ # HTTP controllers
│ │ ├── user_controller.go
│ │ └── autogen_module.go # Generated
│ ├── services/ # Business logic
│ │ ├── user_service.go
│ │ └── autogen_module.go # Generated
│ ├── middleware/ # HTTP middleware
│ │ ├── auth_middleware.go
│ │ └── autogen_module.go # Generated
│ ├── models/ # Data models
│ ├── config/ # Configuration
│ └── parsers/ # Custom parameter parsers
├── pkg/ # Public packages
├── go.mod
└── README.md
// Global middleware with priority (Priority ONLY works with -Global)
//axon::middleware SecurityMiddleware -Global -Priority=1
// Local middleware (no Priority support)
//axon::middleware AuthMiddleware
// Specific routes before parameterized ones
//axon::route GET /users/me -Priority=10
//axon::route GET /users/{id:int} -Priority=50
// Catch-all controllers last
//axon::controller -Priority=999// Global: Security, CORS, Rate Limiting (with -Global flag)
//axon::middleware SecurityMiddleware -Global -Priority=1
// Controller: Authentication, Authorization
//axon::controller -Middleware=AuthMiddleware
// Route: Validation, Caching
//axon::route POST /users -Middleware=ValidationMiddleware//axon::service
//axon::interface // Generates interface for easy mocking
type UserService struct {
//axon::inject
UserRepo UserRepositoryInterface // Use interface for testability
}When you need complex initialization logic or error handling during service creation, use custom constructors:
//axon::service -Constructor=NewDatabaseService
type DatabaseService struct {
Config *config.Config
db *sql.DB
}
// Custom constructor with error handling - no axon::inject needed
func NewDatabaseService(config *config.Config) (*DatabaseService, error) {
db, err := sql.Open("postgres", config.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("database ping failed: %w", err)
}
return &DatabaseService{
Config: config,
db: db,
}, nil
}
//axon::service -Constructor=NewRedisClient -Mode=Singleton
type RedisClient struct {
Config *config.Config
client *redis.Client
}
func NewRedisClient(config *config.Config) (*RedisClient, error) {
client := redis.NewClient(&redis.Options{
Addr: config.RedisAddr,
Password: config.RedisPassword,
DB: config.RedisDB,
})
// Test connection
if err := client.Ping(context.Background()).Err(); err != nil {
return nil, fmt.Errorf("redis connection failed: %w", err)
}
return &RedisClient{
Config: config,
client: client,
}, nil
}Benefits of custom constructors:
- Complex initialization logic with error handling
- Connection validation during startup
- Custom configuration or setup
- Integration with third-party libraries that require specific initialization
Important: When using -Constructor, you take full control of service creation. The axon::inject and axon::init annotations are not used since your custom constructor function handles all dependency injection and initialization.
// Use -Init for services that need startup/shutdown logic
//axon::service -Init
type DatabaseService struct {}
// Use -Init=Background for non-blocking services
//axon::service -Init=Background
type CrawlerService struct {}
// Use -Mode=Transient for request-scoped services
//axon::service -Mode=Transient
type SessionService struct {}
// Use -Constructor for complex initialization
//axon::service -Constructor=NewComplexService
type ComplexService struct {}
// Simple services don't need lifecycle hooks
//axon::service
type UtilityService struct {}// Blocking initialization (database connections, etc.)
//axon::service -Init
type DatabaseService struct {
//axon::inject
Config *config.Config
}
// Non-blocking initialization (background workers, etc.)
//axon::service -Init=Background
type CrawlerService struct {}//axon::controller -Prefix=/api/v1/products -Middleware=AuthMiddleware -Priority=20
type ProductController struct {
//axon::inject
ProductService *services.ProductService
}
//axon::route GET / -Priority=10
func (c *ProductController) ListProducts(ctx echo.Context, query axon.QueryMap) ([]*Product, error) {
limit := query.GetIntDefault("limit", 10)
offset := query.GetIntDefault("offset", 0)
category := query.Get("category")
return c.ProductService.List(limit, offset, category)
}
//axon::route GET /featured -Priority=20
func (c *ProductController) GetFeatured() ([]*Product, error) {
return c.ProductService.GetFeatured()
}
//axon::route GET /{id:uuid.UUID} -Priority=50
func (c *ProductController) GetProduct(id uuid.UUID) (*Product, error) {
return c.ProductService.GetByID(id)
}
//axon::route POST / -Middleware=ValidationMiddleware
func (c *ProductController) CreateProduct(product CreateProductRequest) (*axon.Response, error) {
created, err := c.ProductService.Create(product)
if err != nil {
return nil, err
}
return axon.Created(created).
WithHeader("Location", fmt.Sprintf("/api/v1/products/%s", created.ID)), nil
}
//axon::route PUT /{id:uuid.UUID}
func (c *ProductController) UpdateProduct(id uuid.UUID, product UpdateProductRequest) (*Product, error) {
return c.ProductService.Update(id, product)
}
//axon::route DELETE /{id:uuid.UUID} -Middleware=AdminMiddleware
func (c *ProductController) DeleteProduct(id uuid.UUID) error {
return c.ProductService.Delete(id)
}// Global middleware with priorities (Priority only works with -Global)
//axon::middleware SecurityMiddleware -Global -Priority=1
type SecurityMiddleware struct {}
//axon::middleware CORSMiddleware -Global -Priority=5
type CORSMiddleware struct {}
// Local middleware (no Priority support)
//axon::middleware AuthMiddleware
type AuthMiddleware struct {}
//axon::middleware RateLimitMiddleware
type RateLimitMiddleware struct {}
//axon::middleware LoggingMiddleware -Global -Priority=50
type LoggingMiddleware struct {}Global middleware execution order: Security → CORS → Logging → Handler Route-specific middleware: Applied in the order specified on the route/controller
// Database service - blocking initialization (default Same mode)
//axon::service -Init
type DatabaseService struct {
//axon::inject
Config *config.Config
connected bool
}
func (s *DatabaseService) Start(ctx context.Context) error {
// Blocks application startup until database is connected
fmt.Printf("Connecting to database: %s\n", s.Config.DatabaseURL)
s.connected = true
return nil
}
// Background worker - non-blocking initialization
//axon::service -Init=Background
type CrawlerService struct {}
func (s *CrawlerService) Start(ctx context.Context) error {
// Starts in background, doesn't block application startup
go func() {
for {
select {
case <-ctx.Done():
return
default:
// Background processing
time.Sleep(time.Second)
}
}
}()
return nil
}
// Transient service - new instance per request
//axon::service -Mode=Transient
type SessionService struct {
//axon::inject
DatabaseService *DatabaseService
sessionID string
createdAt time.Time
}
func (s *MetricsCollector) Start(ctx context.Context) error {
// Runs in background, doesn't block application startup
go s.collectMetrics()
return nil
}//axon::route GET /users/{id:int}/posts/{slug:string}
func (c *Controller) GetUserPost(id int, slug string) (*Post, error) {}
//axon::route GET /products/{id:uuid.UUID}
func (c *Controller) GetProduct(id uuid.UUID) (*Product, error) {}
//axon::route GET /search
func (c *Controller) Search(ctx echo.Context, query axon.QueryMap) (*SearchResult, error) {
term := query.Get("q") // string
page := query.GetIntDefault("page", 1) // int with default
active := query.GetBool("active") // bool
price := query.GetFloat64("max_price") // float64
}//axon::route_parser DateRange
func ParseDateRange(c echo.Context, value string) (DateRange, error) {
parts := strings.Split(value, "_")
if len(parts) != 2 {
return DateRange{}, fmt.Errorf("invalid date range format")
}
start, err := time.Parse("2006-01-02", parts[0])
if err != nil {
return DateRange{}, err
}
end, err := time.Parse("2006-01-02", parts[1])
if err != nil {
return DateRange{}, err
}
return DateRange{Start: start, End: end}, nil
}
// Usage
//axon::route GET /sales/{period:DateRange}
func (c *Controller) GetSales(period DateRange) ([]*Sale, error) {
return c.SalesService.GetSalesInRange(period.Start, period.End)
}// Data + Error (most common)
func (c *Controller) GetUser(id int) (*User, error) {
user, err := c.UserService.GetUser(id)
if err != nil {
return nil, axon.ErrNotFound("User not found")
}
return user, nil
}
// Returns: 200 OK with JSON body, or custom HTTP status on axon.HttpError
// Custom Response with full control
func (c *Controller) CreateUser(user User) (*axon.Response, error) {
return &axon.Response{
StatusCode: 201,
Body: user,
Headers: map[string]string{
"Location": "/users/123",
},
}, nil
}
// Error Only (for operations)
func (c *Controller) DeleteUser(id int) error {
return c.UserService.Delete(id)
}
// Returns: 204 No Content on success, custom HTTP status on axon.HttpError// Common HTTP errors
return axon.ErrBadRequest("Invalid input")
return axon.ErrUnauthorized("Authentication required")
return axon.ErrForbidden("Access denied")
return axon.ErrNotFound("Resource not found")
return axon.ErrConflict("Resource already exists")
// Custom HTTP error
return axon.NewHttpError(418, "I'm a teapot")// Fluent response building
return axon.Created(user).
WithHeader("Location", "/users/123").
WithSecureCookie("session", "token", "/", 3600), nil
// Common responses
return axon.OK(data)
return axon.Created(user)
return axon.NoContent()
return axon.RedirectTo("/login")Axon generates autogen_module.go files in each package:
// Code generated by Axon framework. DO NOT EDIT.
package controllers
import (
"go.uber.org/fx"
// ... other imports
)
// Provider functions
func NewUserController(userService *services.UserService) *UserController {}
// Route wrappers
func wrapUserControllerGetUser(controller *UserController) echo.HandlerFunc {}
// Route registration with middleware
func RegisterRoutes(e *echo.Echo, userController *UserController, authMiddleware *AuthMiddleware) {
userGroup := e.Group("/api/v1/users")
userGroup.GET("/search", wrapUserControllerSearchUsers(userController), authMiddleware.Handle)
userGroup.GET("/:id", wrapUserControllerGetUser(userController), authMiddleware.Handle)
}
// FX Module
var AutogenModule = fx.Module("controllers",
fx.Provide(NewUserController),
fx.Invoke(RegisterRoutes),
)Use generated modules in your application:
package main
import (
"github.com/your-app/internal/controllers"
"github.com/your-app/internal/services"
"github.com/your-app/internal/middleware"
"go.uber.org/fx"
)
func main() {
fx.New(
// Generated modules
controllers.AutogenModule,
services.AutogenModule,
middleware.AutogenModule,
// Manual providers
fx.Provide(
config.New,
echo.New,
),
// Start HTTP server
fx.Invoke(startServer),
).Run()
}We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Add tests for new functionality
- Ensure all tests pass (
go test ./...) - Submit a pull request
MIT License - see LICENSE file for details.
Ready to build something amazing? Check out our complete example application to see Axon in action!