Structuring a Go Project with Webserver (GoFiber) and CLI Components
When developing a Go application that includes both a web server (using GoFiber) and a command-line interface (CLI), it's beneficial to structure the project in a way that promotes code reuse, consistency, and maintainability. A common and recommended approach is to keep both components within the same repository, allowing them to share internal logic and resources effectively.
Benefits of a Single Repository
Having both the webserver and CLI components in the same repository offers several advantages:
- Shared Internal Components: The
internaldirectory facilitates the sharing of common logic, models, and services between the web and CLI interfaces. This ensures consistent behavior and reduces code duplication. - Consistent Dependency Management: A single
go.modfile manages all dependencies for both components, simplifying dependency management and version control. - Easier Maintenance: A centralized codebase makes it simpler to ensure consistency and make cross-cutting changes, streamlining maintenance and updates.
- Simplified Collaboration: Teams (or individuals) working on both parts can easily reference and modify shared code, improving collaboration.
Recommended Directory Structure
Here's a recommended directory structure that incorporates best practices for organizing your Go project:
project-root/
├── cmd/
│ ├── webserver/
│ │ └── main.go # Web server entry point
│ └── cli/
│ └── main.go # CLI entry point
│
├── internal/
│ ├── auth/ # Authentication logic
│ │ ├── handlers.go # Web handlers for auth
│ │ └── service.go # Business logic for auth
│ ├── models/ # Data structures used throughout
│ │ └── user.go
│ ├── services/ # Core business logic
│ │ ├── authentication.go
│ │ ├── user_service.go
│ │ └── ...
│ ├── utils/ # Shared utilities
│ │ └── logging.go
│ └── ...
│
├── pkg/ # Potentially reusable packages (optional)
│ └── ...
│
├── configs/ # Configuration files
│ └── config.yaml
│
├── scripts/ # Scripts for setup, migration, etc.
│ └── ...
│
├── go.mod
├── go.sum
└── README.md
Explanation of Directories:
-
cmd/: This directory contains the main entry points for your executables. It is further divided into:webserver/: Contains themain.gofile that initializes and runs the GoFiber web server.cli/: Contains themain.gofile that handles command-line argument parsing and execution.
-
internal/: This directory houses the shared business logic and utilities that both the web server and CLI will use. It is further divided into:auth/: Contains authentication-related logic, including handlers for web requests and services for business logic.models/: Contains data structures used throughout the application.services/: Contains core business logic, such as authentication and user management.utils/: Contains shared utility functions, such as logging.
-
pkg/: (Optional) If you have any utility functions or types that you could potentially share with external projects, you can place them here. Note that packages withinpkg/are not restricted to this project. -
configs/: Contains configuration files for your application, which can be loaded by both components. -
scripts/: Contains any additional scripts for tasks like database migrations, build scripts, or deployment scripts. -
go.mod: The Go module file that includes your project's dependencies.
Example Code Snippets
Webserver Component (cmd/webserver/main.go)
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"your-project/internal/utils/logging"
"your-project/internal/handlers"
"your-project/internal/services"
"net/http"
)
func main() {
app := fiber.New(fiber.Config{
AppName: "Fiber with Planetscale",
ServerHeader: "Fiber",
})
app.Use(logger.New())
authService := services.NewAuthService()
app.Post("/login", handlers.LoginHandler(authService))
app.Get("/", func(c *fiber.Ctx) error {
return c.Status(http.StatusOK).JSON(&fiber.Map{
"message": "Hello world",
})
})
app.Listen(":3000")
}
CLI Component (cmd/cli/main.go)
package main
import (
"flag"
"fmt"
"log"
"your-project/internal/utils/logging"
"your-project/internal/services"
"your-project/internal/handlers"
)
func main() {
flag.Parse()
log.Println("CLI component started")
authService := services.NewAuthService()
// Example CLI command using the same auth service
// You would implement CLI-specific command handling here
// For example:
// handlers.CLILogin(authService, "username", "password")
fmt.Println("CLI login command executed")
}
Shared Authentication Service (internal/services/authentication.go)
package services
type AuthService struct {
// Shared authentication logic
}
func NewAuthService() *AuthService {
return &AuthService{}
}
func (s *AuthService) Login(username, password string) (string, error) {
// Consistent login logic used by both web and CLI
// Example:
if username == "test" && password == "password" {
return "valid_token", nil
}
return "", fmt.Errorf("invalid credentials")
}
func (s *AuthService) Logout(token string) error {
// Consistent logout logic
return nil
}
Shared Authentication Handler (internal/handlers/auth.go)
package handlers
import (
"github.com/gofiber/fiber/v2"
"your-project/internal/services"
"net/http"
)
func LoginHandler(authService *services.AuthService) fiber.Handler {
return func(c *fiber.Ctx) error {
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
var req LoginRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
}
token, err := authService.Login(req.Username, req.Password)
if err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
}
return c.Status(http.StatusOK).JSON(fiber.Map{"token": token})
}
}
func CLILogin(authService *services.AuthService, username, password string) (string, error) {
token, err := authService.Login(username, password)
if err != nil {
return "", err
}
return token, nil
}
Key Considerations
- google's wire
-
Error Handling: Implement a consistent error handling strategy across your webserver and CLI. Consider using custom error types or a dedicated error handling package.
-
Testing: Write comprehensive unit and integration tests for your
internalcomponents to ensure consistency and prevent regressions. -
Modularity: Structure your code in a modular way, separating concerns into different packages and directories. This makes your code easier to understand, maintain, and test.
Conclusion
By structuring your project in this manner, you ensure that both the web and CLI components share the same internal components, thereby maintaining consistency in handling common actions and response messages across both interfaces. This approach promotes code reuse, simplifies maintenance, and enhances the overall quality of your application.
For further information, you can refer to these resources:
Last updated January 6, 2025