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/auroraThe framework includes sample projects under sample:
| Sample | Description |
|---|---|
| sample/full_showcase | Full application: Server, GORM, Redis, JWT, i18n, migrations, layered structure (controller / service / datalayer), RBAC, and auth. Use this as a reference for building a complete Aurora app. |
| sample/customize_error_handler | Minimal app that demonstrates custom error response format: implement contracts.ErrorHandler and pass it via feature.WithErrorHandler() so all handler errors use your own JSON shape. |
Run an example from the Aurora repo root (e.g. cd sample then the run command in that sample’s README). See each sample’s README for required environment variables and run commands.
package 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 atfeature/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 (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",
})Custom Error JSON Structure
By default, error responses use the format {"message": "..."} with the HTTP status code from bizerr.BizError. To use your own error response format (e.g. custom fields, error codes, or i18n), implement the contracts.ErrorHandler interface and pass it when creating the server:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/shyandsy/aurora/app"
"github.com/shyandsy/aurora/contracts"
"github.com/shyandsy/aurora/feature"
"github.com/shyandsy/aurora/bizerr"
)
// MyErrorHandler implements contracts.ErrorHandler to customize error response JSON.
type MyErrorHandler struct{}
func (MyErrorHandler) HandleError(c *gin.Context, err error) {
code := http.StatusInternalServerError
msg := err.Error()
if e, ok := err.(bizerr.BizError); ok {
code = e.HTTPCode()
msg = e.Message()
}
c.JSON(code, gin.H{
"code": code,
"message": msg,
"error": err.Error(),
// Add any custom fields you need
})
}
func main() {
a := app.NewApp()
a.AddFeature(feature.NewServerFeature(
feature.WithErrorHandler(MyErrorHandler{}),
))
a.AddFeature(feature.NewGormFeature())
// ... register routes and run
}- If you do not pass
WithErrorHandler, the default format{"message": "..."}is used. - If you pass
WithErrorHandler(handler), all handler errors are sent using yourHandleError(c, err)implementation, so you control the full JSON body and status code.
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