Documentation

docs
core
application

Application Structure

Application Structure

Planned feature

Some sections below describe a planned Laravel-style layout (pkg/core, artisan). The current framework uses main.go, routes/register.go, and internal/* as documented in the Quick Start.

Mithril follows a modular architecture with clear separation between routes, internal handlers, and the database layer.

Directory Structure#

A typical Mithril application follows this structure:

my-app/
├── app/                    # Application code
│   ├── controllers/       # HTTP controllers
│   ├── middleware/        # Custom middleware
│   ├── models/           # Database models
│   ├── schemas/          # Request/response validation
│   └── modules/          # Django-style modules
├── config/               # Configuration files
├── database/
│   ├── migrations/       # Database migrations
│   └── seeders/         # Database seeders
├── routes/              # Route definitions
│   ├── api.go          # API routes
│   ├── web.go          # Web routes
│   └── console.go      # Console commands
├── templates/          # HTML templates
├── utils/             # Utility functions
├── artisan           # Local CLI script
├── main.go           # Application entry point
├── Makefile         # Build commands
├── Dockerfile       # Docker configuration
└── docker-compose.*.yml # Docker Compose files

Core Components#

1. Application Entry Point (main.go)

The main.go file is the entry point of your Mithril application:

package main

import (
    "log"
    "github.com/mithril-framework/mithril/pkg/core"
    "my-app/routes"
)

func main() {
    // Create new application instance
    app := core.NewApplication()
    
    // Setup routes
    routes.SetupAPIRoutes(app)
    routes.SetupWebRoutes(app)
    
    // Start the server
    log.Fatal(app.Listen(":3000"))
}

2. Application Core (pkg/core)

The core package provides the foundation of Mithril:

// pkg/core/application.go
type Application struct {
    *fiber.App
    config *config.Manager
    container *container.Container
}

func NewApplication() *Application {
    app := &Application{
        App: fiber.New(fiber.Config{
            AppName: "Mithril App",
        }),
    }
    
    // Initialize configuration
    app.config = config.NewManagerFromEnv()
    
    // Initialize dependency injection container
    app.container = container.New()
    
    // Setup middleware
    app.setupMiddleware()
    
    return app
}

3. Configuration Management (config/)

Configuration is managed through environment variables and structured config files:

// config/app.go
type AppConfig struct {
    Name        string `env:"APP_NAME" default:"Mithril App"`
    Environment string `env:"APP_ENV" default:"development"`
    Debug       bool   `env:"APP_DEBUG" default:"false"`
    Host        string `env:"APP_HOST" default:"localhost"`
    Port        int    `env:"APP_PORT" default:"3000"`
}

// config/database.go
type DatabaseConfig struct {
    Driver   string `env:"DB_DRIVER" default:"postgres"`
    Host     string `env:"DB_HOST" default:"localhost"`
    Port     int    `env:"DB_PORT" default:"5432"`
    Name     string `env:"DB_NAME" required:"true"`
    User     string `env:"DB_USER" required:"true"`
    Password string `env:"DB_PASSWORD" required:"true"`
}

4. Dependency Injection Container (pkg/core/container)

Mithril includes a service container for dependency injection:

// pkg/core/container/container.go
type Container struct {
    services map[string]interface{}
    singletons map[string]interface{}
}

func (c *Container) Bind(name string, resolver interface{}) {
    c.services[name] = resolver
}

func (c *Container) Singleton(name string, resolver interface{}) {
    c.singletons[name] = resolver
}

func (c *Container) Make(name string) interface{} {
    // Resolve service from container
}

Application Layers#

1. Controllers (app/controllers/)

Controllers handle HTTP requests and responses:

// app/controllers/user_controller.go
type UserController struct {
    userService *services.UserService
}

func NewUserController(userService *services.UserService) *UserController {
    return &UserController{
        userService: userService,
    }
}

func (c *UserController) Index(ctx *fiber.Ctx) error {
    users, err := c.userService.GetAll()
    if err != nil {
        return ctx.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    
    return ctx.JSON(fiber.Map{"data": users})
}

func (c *UserController) Store(ctx *fiber.Ctx) error {
    var req schemas.CreateUserRequest
    if err := ctx.BodyParser(&req); err != nil {
        return ctx.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    
    user, err := c.userService.Create(req)
    if err != nil {
        return ctx.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    
    return ctx.Status(201).JSON(fiber.Map{"data": user})
}

2. Models (app/models/)

Models represent your database entities:

// app/models/user.go
type User struct {
    ID        uuid.UUID `json:"id" gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    Email     string    `json:"email" gorm:"uniqueIndex;not null"`
    Name      string    `json:"name" gorm:"not null"`
    Password  string    `json:"-"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}

// Relationships
func (u *User) Posts() []Post {
    var posts []Post
    db.Model(u).Association("Posts").Find(&posts)
    return posts
}

3. Schemas (app/schemas/)

Schemas define request/response validation:

// app/schemas/user_request.go
type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email" example:"user@example.com"`
    Password string `json:"password" validate:"required,min=8" example:"password123"`
    Name     string `json:"name" validate:"required" example:"John Doe"`
}

type UpdateUserRequest struct {
    Name string `json:"name" validate:"required" example:"John Doe"`
}

// app/schemas/user_response.go
type UserResponse struct {
    ID        uuid.UUID `json:"id"`
    Email     string    `json:"email"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

4. Middleware (app/middleware/)

Custom middleware for request processing:

// app/middleware/auth.go
func AuthMiddleware() fiber.Handler {
    return func(c *fiber.Ctx) error {
        token := c.Get("Authorization")
        if token == "" {
            return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
        }
        
        // Validate JWT token
        claims, err := jwt.ValidateToken(token)
        if err != nil {
            return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
        }
        
        // Set user in context
        c.Locals("user", claims.User)
        
        return c.Next()
    }
}

Route Organization#

1. API Routes (routes/api.go)

// routes/api.go
func SetupAPIRoutes(app *fiber.App) {
    api := app.Group("/api")
    
    // Public routes
    api.Post("/auth/login", authController.Login)
    api.Post("/auth/register", authController.Register)
    
    // Protected routes
    protected := api.Group("/", middleware.AuthMiddleware())
    protected.Get("/users", userController.Index)
    protected.Post("/users", userController.Store)
    protected.Get("/users/:id", userController.Show)
    protected.Put("/users/:id", userController.Update)
    protected.Delete("/users/:id", userController.Delete)
}

2. Web Routes (routes/web.go)

// routes/web.go
func SetupWebRoutes(app *fiber.App) {
    // Serve static files
    app.Static("/", "./public")
    
    // Web routes
    app.Get("/", webController.Home)
    app.Get("/about", webController.About)
    app.Get("/contact", webController.Contact)
}

Module System#

Mithril supports Django-style modules for organizing related functionality:

1. Creating a Module

# Create a complete module
go run . artisan make:module blog --full

This creates:

app/modules/blog/
├── controllers/
│   ├── post_controller.go
│   └── category_controller.go
├── models/
│   ├── post.go
│   └── category.go
├── schemas/
│   ├── post_request.go
│   ├── post_response.go
│   ├── category_request.go
│   └── category_response.go
├── routes.go
└── README.md

2. Module Routes

// app/modules/blog/routes.go
func SetupBlogRoutes(app *fiber.App) {
    blog := app.Group("/blog")
    
    // Post routes
    blog.Get("/posts", postController.Index)
    blog.Post("/posts", postController.Store)
    blog.Get("/posts/:id", postController.Show)
    blog.Put("/posts/:id", postController.Update)
    blog.Delete("/posts/:id", postController.Delete)
    
    // Category routes
    blog.Get("/categories", categoryController.Index)
    blog.Post("/categories", categoryController.Store)
}

Service Layer#

While not required, you can create a service layer for business logic:

// app/services/user_service.go
type UserService struct {
    userRepo *repositories.UserRepository
}

func NewUserService(userRepo *repositories.UserRepository) *UserService {
    return &UserService{
        userRepo: userRepo,
    }
}

func (s *UserService) GetAll() ([]models.User, error) {
    return s.userRepo.FindAll()
}

func (s *UserService) Create(req schemas.CreateUserRequest) (*models.User, error) {
    // Hash password
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    if err != nil {
        return nil, err
    }
    
    user := &models.User{
        Email:    req.Email,
        Name:     req.Name,
        Password: string(hashedPassword),
    }
    
    return s.userRepo.Create(user)
}

Best Practices#

1. Separation of Concerns

  • Keep controllers thin
  • Move business logic to services
  • Use repositories for data access

2. Error Handling

  • Use consistent error responses
  • Log errors appropriately
  • Return meaningful error messages

3. Code Organization

  • Group related functionality in modules
  • Use meaningful names for files and functions
  • Keep functions small and focused

4. Configuration

  • Use environment variables for configuration
  • Provide sensible defaults
  • Validate configuration on startup

5. Testing

  • Write unit tests for business logic
  • Use integration tests for API endpoints
  • Mock external dependencies

Next Steps#

Now that you understand the application structure, explore these topics: