A lightweight, modular web framework for Go, built on top of Gin with dependency injection support.
- 🚀 Modular Architecture: Feature-based design for easy extensibility
- 🔌 Dependency Injection: Built-in DI container (
github.com/shyandsy/di) for clean dependency management - 🗄️ Database Support: GORM integration with MySQL and SQLite drivers
- 🔐 JWT Authentication: Built-in JWT token generation, validation, and refresh with Redis blacklist support
- 📦 Redis Support: Redis integration with service interface for caching and session management
- 🔄 Database Migrations: Goose-based migration system with automatic version tracking
- ⚙️ Configuration Management: Environment-based configuration loading with validation
- 🛡️ Error Handling: Unified business error handling with validation error support
- 🌐 CORS Support: Configurable CORS middleware
- 🔒 Route Middlewares: Support for route-specific Gin middlewares (e.g., JWT authentication, rate limiting)
- 🏥 Health Checks: Built-in
/healthand/readyendpoints - 📝 Request Context: Extended request context with App instance for easy dependency access
- 🌍 Internationalization (i18n): Multi-language support using go-i18n with automatic language detection
- 📊 Structured Logging: Built-in logger with log levels (Error, Info, Debug) and environment-based configuration
go get github.com/shyandsy/aurorapackage main
import (
"log"
"github.com/shyandsy/aurora/bootstrap"
"github.com/shyandsy/aurora/contracts"
"github.com/shyandsy/aurora/bizerr"
)
func main() {
// Create application with default features (Server, GORM, Redis, JWT)
app := bootstrap.InitDefaultApp()
// Register routes
app.RegisterRoutes([]contracts.Route{
{
Method: "GET",
Path: "/hello",
Handler: func(c *contracts.RequestContext) (interface{}, bizerr.BizError) {
return map[string]string{"message": "Hello, Aurora!"}, nil
},
},
})
// Run application
if err := app.Run(); err != nil {
log.Fatalf("Failed to run app: %v", err)
}
}package main
import (
"log"
"github.com/shyandsy/aurora/app"
"github.com/shyandsy/aurora/feature"
"github.com/shyandsy/aurora/contracts"
"github.com/shyandsy/aurora/bizerr"
)
func main() {
// Create application
a := app.NewApp()
// Add features manually
a.AddFeature(feature.NewServerFeature())
a.AddFeature(feature.NewGormFeature())
a.AddFeature(feature.NewRedisFeature())
a.AddFeature(feature.NewJWTFeature())
// Register routes
a.RegisterRoutes([]contracts.Route{
{
Method: "GET",
Path: "/api/users",
Handler: getUserHandler,
},
})
// Run application
if err := a.Run(); err != nil {
log.Fatalf("Failed to run app: %v", err)
}
}
func getUserHandler(c *contracts.RequestContext) (interface{}, bizerr.BizError) {
// Access App instance directly from context
var userService UserService
if err := c.App.Find(&userService); err != nil {
return nil, bizerr.ErrInternalServerError(err)
}
// Your handler logic
return map[string][]string{"users": {"user1", "user2"}}, nil
}Aurora uses environment variables for configuration. All configurations are validated on startup.
HOST: Server host (default:0.0.0.0)PORT: Server port (default:8080)SERVICE_NAME: Service name (required)SERVICE_VERSION: Service version (default:1.0.0)RUN_LEVEL: Run level -local,stage, orproduction(default:local)READ_TIMEOUT: Read timeout (default:30s)WRITE_TIMEOUT: Write timeout (default:30s)SHUTDOWN_TIMEOUT: Graceful shutdown timeout (default:5s)
Note: Gin mode is automatically set based on RUN_LEVEL:
production→releasemodelocalorstage→debugmode
DB_DRIVER: Database driver -mysqlorsqlite(required)DB_DSN: Database connection string (required)DB_MAX_IDLE_CONNS: Maximum idle connections (required, must be > 0)DB_MAX_OPEN_CONNS: Maximum open connections (required, must be > 0, must be >= DB_MAX_IDLE_CONNS)
Example:
DB_DRIVER=mysql
DB_DSN=user:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
DB_MAX_IDLE_CONNS=10
DB_MAX_OPEN_CONNS=100REDIS_ADDR: Redis address (required, format:host:port)REDIS_PASSWORD: Redis password (required)REDIS_DB: Redis database number (required, must be >= 0)
Example:
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=yourpassword
REDIS_DB=0JWT_SECRET: JWT secret key (required, must be changed from default in production)JWT_EXPIRE_TIME: Access token expiry duration (required, e.g.,15m,1h)JWT_ISSUER: JWT issuer identifier (required)
Example:
JWT_SECRET=your-super-secret-jwt-key-here-change-in-production
JWT_EXPIRE_TIME=15m
JWT_ISSUER=myappNote: Refresh tokens expire after JWT_EXPIRE_TIME * 24 (e.g., 15m * 24 = 6 hours).
Configure internationalization settings:
# Default language (required)
I18N_DEFAULT_LANG=en
# Supported languages (comma-separated, required)
I18N_SUPPORTED_LANGS=en,zh-CN,ja
# Application locale files directory (relative to working directory, optional)
# Framework locale files are embedded in the binary and loaded automatically
I18N_LOCALE_DIR=localesImportant Notes:
- Framework locale files are embedded in the Aurora binary using
go:embedand are always loaded automatically. They are located atapi/aurora/feature/i18n/in the source code. - Application locale files should be placed in the directory specified by
I18N_LOCALE_DIR(relative to your application's working directory). - Application locale files can override framework messages with the same message ID.
Locale File Format:
Create language files using flat structure (not nested). The framework supports multiple formats with the following priority:
- YAML (
.yamlor.yml) - Recommended, most readable - TOML (
.toml) - JSON (
.json)
Framework locale file example (api/aurora/feature/i18n/en.yaml):
error.not_found:
id: error.not_found
other: Resource not found
error.internal_server:
id: error.internal_server
other: Internal server error
error.validation:
id: error.validation
other: "Validation error: {{.Message}}"
error.bad_request:
id: error.bad_request
other: Bad request
error.unauthorized:
id: error.unauthorized
other: Unauthorized
error.forbidden:
id: error.forbidden
other: ForbiddenApplication locale file example (locales/en.yaml):
welcome:
id: welcome
other: Welcome to Customer Service
user.email_exists:
id: user.email_exists
other: Email already exists
user.invalid_email:
id: user.invalid_email
other: Invalid email format
auth.register_success:
id: auth.register_success
other: Registration successfulNote: Use flat structure with dot notation (e.g., user.email_exists:) instead of nested structure (e.g., user: email_exists:). This ensures compatibility with go-i18n's message parsing.
Example TOML file (locales/en.toml):
[welcome]
id = "welcome"
other = "Welcome to Customer Service"
[user.email_exists]
id = "user.email_exists"
other = "Email already exists"
[error.validation]
id = "error.validation"
other = "Validation error: {{.Message}}"GOOSE_TABLE_PREFIX: Optional prefix for goose version table name (optional)- If set, goose version table will use this prefix (e.g.,
admin_goose_db_version) - If not set or empty, goose default table name
goose_db_versionwill be used
- If set, goose version table will use this prefix (e.g.,
Example:
# Use default table name "goose_db_version"
# (no environment variable needed)
# Use custom table name "admin_goose_db_version"
GOOSE_TABLE_PREFIX=admin_CORS_ALLOWED_ORIGINS: Comma-separated list of allowed origins (optional)CORS_ALLOWED_METHODS: Comma-separated list of allowed HTTP methods (optional)CORS_ALLOWED_HEADERS: Comma-separated list of allowed headers (optional)CORS_ALLOWED_CREDENTIALS: Allow credentials (optional,trueorfalse)
Note: CORS is only enabled if at least one CORS configuration is provided.
Aurora provides a built-in structured logger with three log levels:
LOG_LEVEL: Log level -error,info, ordebug(optional, highest priority)RUN_LEVEL: Automatically determines log level ifLOG_LEVELis not set
Log Level Priority:
LOG_LEVELenvironment variable (if set, takes highest priority)RUN_LEVELenvironment variable (ifLOG_LEVELis not set):local→debug(all messages logged)stage→info(error and info messages logged)production→error(only error messages logged)
- Default:
error(if neitherLOG_LEVELnorRUN_LEVELis set)
Log Levels:
error: Only error messages are logged (suitable for production)info: Error and info messages are loggeddebug: All messages (error, info, and debug) are logged
Examples:
# Option 1: Explicitly set LOG_LEVEL (highest priority)
LOG_LEVEL=debug
# Option 2: Let RUN_LEVEL determine log level automatically
RUN_LEVEL=local # → debug level
RUN_LEVEL=stage # → info level
RUN_LEVEL=production # → error level
# Option 3: Override RUN_LEVEL with explicit LOG_LEVEL
RUN_LEVEL=production
LOG_LEVEL=debug # → debug level (LOG_LEVEL takes priority)Note: If neither LOG_LEVEL nor RUN_LEVEL is set, the logger will use error level by default and print a message indicating the default log level being used.
The contracts.App interface provides:
AddFeature(feature Features): Register a featureRegisterRoutes(routes []contracts.Route): Register API routesRun() error: Start the applicationShutdown() error: Gracefully shutdown the applicationGetContainer() di.Container: Get the DI container- Direct access to
di.Containermethods (Provide, Resolve, Find, etc.)
Aurora provides contracts.RequestContext which extends gin.Context with the App instance and Translator:
type RequestContext struct {
*gin.Context
App contracts.App
Translator contracts.Translator
}This allows handlers to directly access the App instance, DI container, and translation service without global variables or context lookups.
Language Detection:
The RequestContext automatically detects the language from:
- Query parameter
lang(e.g.,?lang=zh-CN) Accept-LanguageHTTP header- Default language from configuration
Translation in Handlers:
func myHandler(c *contracts.RequestContext) (interface{}, bizerr.BizError) {
// Use the built-in T method for translation
message := c.T("welcome")
// Translation with variables
errorMsg := c.T("error.validation", map[string]interface{}{
"Message": "Email is required",
})
return map[string]string{
"message": message,
"error": errorMsg,
}, nil
}Features implement the contracts.Features interface:
Name() string: Feature identifierSetup(app App) error: Initialize the featureClose() error: Cleanup resources
-
ServerFeature: HTTP server with routing, health checks, and graceful shutdown
- Automatically registers
/healthand/readyendpoints - Supports graceful shutdown with configurable timeout
- Handles SIGINT and SIGTERM signals
- Creates
RequestContextfor each request with App instance
- Automatically registers
-
GormFeature: GORM database connection
- Supports MySQL and SQLite
- Configurable connection pool
- Provides both
*gorm.DBand*sql.DBto DI container
-
RedisFeature: Redis client with service interface
- Provides
feature.RedisServiceinterface to DI container - Methods:
Get,Set,Delete,Exists
- Provides
-
JWTFeature: JWT token management
- Token generation and validation
- Refresh token support
- Token blacklist using Redis
- Provides
feature.JWTServiceinterface to DI container
-
I18NFeature: Internationalization support
- Multi-language translation using go-i18n
- Automatic language detection from HTTP headers and query parameters
- Supports YAML (recommended), TOML, and JSON locale files
- Framework locale files are embedded in the binary using
go:embed(always available) - Application locale files loaded from configured directory (can override framework messages)
- Provides
contracts.Translatorinterface to DI container - Integrated with
RequestContextfor easy translation in handlers
-
Logger: Structured logging support
- Three log levels: Error (always logged), Info, Debug
- Environment-based configuration via
LOG_LEVEL(explicit) orRUN_LEVEL(automatic) - Automatic log level selection based on
RUN_LEVELifLOG_LEVELis not set - Error logs go to stderr, Info/Debug logs go to stdout
- Includes timestamp and file location in log output
- Global logger functions available without initialization
Routes use contracts.CustomizedHandlerFunc signature:
type CustomizedHandlerFunc func(*RequestContext) (interface{}, bizerr.BizError)The contracts.Route struct supports:
Method: HTTP method (GET, POST, PUT, DELETE, PATCH)Path: Route pathHandler: CustomizedHandlerFunc for business logicMiddlewares: Optional slice ofgin.HandlerFuncfor route-specific middleware
Middleware Support:
You can attach Gin middlewares to specific routes. Middlewares are executed in the order they are defined, before the main handler:
app.RegisterRoutes([]contracts.Route{
{
Method: "GET",
Path: "/public",
Handler: publicHandler,
// No middleware - public endpoint
},
{
Method: "GET",
Path: "/protected",
Handler: protectedHandler,
Middlewares: []gin.HandlerFunc{jwtAuthMiddleware, rateLimitMiddleware},
// Middlewares execute in order: jwtAuthMiddleware → rateLimitMiddleware → protectedHandler
},
})Handlers receive *contracts.RequestContext which:
- Embeds
*gin.Context- all Gin methods are available - Contains
App contracts.App- direct access to DI container
Handlers return:
(data, nil): Success response (HTTP 200)(nil, bizErr): Error response (HTTP code frombizErr.HTTPCode())
Example:
import (
"errors"
"github.com/shyandsy/aurora/contracts"
"github.com/shyandsy/aurora/bizerr"
)
func getUserHandler(c *contracts.RequestContext) (interface{}, bizerr.BizError) {
userID := c.Param("id") // Gin method available
if userID == "" {
return nil, bizerr.ErrBadRequest(errors.New("user ID is required"))
}
// Access DI container directly
var userService UserService
if err := c.App.Find(&userService); err != nil {
return nil, bizerr.ErrInternalServerError(err)
}
// Your business logic
user := userService.GetUser(userID)
if user == nil {
return nil, bizerr.ErrNotFound()
}
return user, nil
}Aurora provides unified error handling through bizerr.BizError:
// Standard errors
bizerr.ErrBadRequest(err)
bizerr.ErrUnauthorized()
bizerr.ErrForbidden()
bizerr.ErrNotFound()
bizerr.ErrInternalServerError(err)
// Validation errors
bizerr.NewValidationError("message", map[string]string{
"field1": "error message 1",
"field2": "error message 2",
})
// Single field validation
bizerr.NewSingleFieldError("email", "invalid email format")
// Multiple field validation
bizerr.NewMultipleFieldErrors(map[string]string{
"email": "invalid email",
"password": "password too short",
})Migrations are automatically run on startup when using bootstrap.InitDefaultApp().
Migration files should be placed in the migrations/ directory relative to the working directory.
Migration Configuration:
GOOSE_TABLE_PREFIX: Optional prefix for goose version table name (optional)- If set, goose version table will use this prefix (e.g.,
admin_goose_db_version) - If not set or empty, goose default table name
goose_db_versionwill be used
- If set, goose version table will use this prefix (e.g.,
Example:
# Use default table name "goose_db_version"
# (no environment variable needed)
# Use custom table name "admin_goose_db_version"
GOOSE_TABLE_PREFIX=admin_Migration File Format:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS users;
-- +goose StatementEndAfter migrations complete, the current migration version is logged.
Aurora integrates github.com/shyandsy/di for dependency injection:
// Provide dependencies
app.Provide(&myService)
// Resolve dependencies
var service *MyService
app.Resolve(&service)
// Provide with interface
app.ProvideAs(impl, (*MyInterface)(nil))
// Resolve with interface
var service MyInterface
app.Find(&service)In Handlers:
func myHandler(c *contracts.RequestContext) (interface{}, bizerr.BizError) {
// Access DI container directly from RequestContext
var service MyService
if err := c.App.Find(&service); err != nil {
return nil, bizerr.ErrInternalServerError(err)
}
// Use service
return service.DoSomething(), nil
}Features can use struct tags for automatic injection:
type MyFeature struct {
Config *config.ServerConfig `inject:""`
DB *gorm.DB `inject:""`
RedisSvc feature.RedisService `inject:""`
}Implement the contracts.Features interface to create custom features:
type MyFeature struct {
Config *config.ServerConfig `inject:""`
DB *gorm.DB `inject:""`
}
func NewMyFeature() contracts.Features {
return &MyFeature{}
}
func (f *MyFeature) Name() string {
return "myfeature"
}
func (f *MyFeature) Setup(app contracts.App) error {
// Resolve dependencies
if err := app.Resolve(f); err != nil {
return err
}
// Initialize your feature
// Provide services to DI container
app.Provide(f)
return nil
}
func (f *MyFeature) Close() error {
// Cleanup resources
return nil
}Aurora provides a built-in structured logger that can be used throughout your application:
import "github.com/shyandsy/aurora/logger"
func myHandler(c *contracts.RequestContext) (interface{}, bizerr.BizError) {
// Log error (always logged regardless of log level)
logger.Error("Failed to process request: %+v", err)
// Log info (logged when LOG_LEVEL is info or debug)
logger.Info("Processing request for user: %s", userID)
// Log debug (only logged when LOG_LEVEL is debug)
logger.Debug("Request details: %+v", requestData)
// Alternative format functions
logger.Errorf("Error: %s", err.Error())
logger.Infof("Info: %s", message)
logger.Debugf("Debug: %s", debugInfo)
}Log Output Format:
[ERROR] 2024/12/08 14:30:45 service.go:123: Failed to process request: database connection failed
[INFO] 2024/12/08 14:30:45 handler.go:45: Processing request for user: 12345
[DEBUG] 2024/12/08 14:30:45 handler.go:46: Request details: map[method:GET path:/api/users]
Best Practices:
- Use
logger.Error()for errors that need attention (always logged) - Use
logger.Info()for important application events - Use
logger.Debug()for detailed debugging information - Include context variables in log messages (e.g.,
orderNo,customerID,error=%+v) - Use
%+vformat for errors to include stack traces when available
Example with Context:
func cancelOrder(ctx *contracts.RequestContext, orderNo string, customerID int64) bizerr.BizError {
// Log error with context variables
logger.Error("CancelOrder: failed to get order, orderNo=%s, customerID=%d, error=%+v",
orderNo, customerID, err)
// Return generic error to client (don't expose database details)
msg := ctx.T("error.internal_server")
return bizerr.ErrInternalServerError(errors.New(msg))
}Aurora automatically registers two health check endpoints:
GET /health: Returns service status, name, version, and timestampGET /ready: Returns service readiness status
MIT