Echo Project Structure
When starting a new web application with Echo, one of the first decisions you'll face is how to structure your project. A well-organized project structure improves maintainability, makes collaboration easier, and helps you scale your application as it grows. This guide will walk you through recommended project structures for Echo applications.
Introduction to Echo Project Structure
The Echo framework is minimalist by design, giving developers flexibility in how they organize their code. Unlike some frameworks that enforce strict directory structures, Echo allows you to structure your application based on your specific needs and preferences.
That said, following certain conventions and patterns can make your Echo applications more maintainable and easier to understand, especially as they grow in complexity.
Basic Echo Project Structure
For a small Echo application, a simple structure might look like this:
my-echo-app/
├── main.go # Application entry point
├── handlers/ # Request handlers
├── models/ # Data models
├── static/ # Static files (CSS, JS, images)
├── templates/ # HTML templates
└── go.mod # Go module file
Let's create a simple Echo application with this structure.
Example: Creating a Basic Echo Application
First, let's initialize our Go module:
mkdir my-echo-app
cd my-echo-app
go mod init my-echo-app
go get github.com/labstack/echo/v4
Next, let's create our directory structure:
mkdir -p handlers models static templates
Now, let's create a basic Echo server in main.go
:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"my-echo-app/handlers"
)
func main() {
// Create a new Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", handlers.HomeHandler)
e.GET("/users", handlers.ListUsersHandler)
e.GET("/users/:id", handlers.GetUserHandler)
// Static files
e.Static("/static", "static")
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
In the handlers
directory, create a file called home_handler.go
:
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
)
// HomeHandler handles requests to the home page
func HomeHandler(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to our Echo application!")
}
// ListUsersHandler returns a list of users
func ListUsersHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "This would return a list of users",
})
}
// GetUserHandler returns a specific user by ID
func GetUserHandler(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"user_id": id,
"message": "This would return user details",
})
}
This basic structure works well for simple applications, but as your project grows, you'll want a more organized approach.
Recommended Structure for Larger Applications
For medium to large applications, a more comprehensive structure is recommended:
my-echo-app/
├── cmd/
│ └── api/
│ └── main.go # Application entry point
├── internal/
│ ├── api/ # API layer
│ │ ├── handlers/ # HTTP handlers
│ │ ├── middlewares/ # Custom middlewares
│ │ └── routes/ # Route definitions
│ ├── config/ # Configuration
│ ├── domain/ # Business logic and entities
│ │ ├── user/
│ │ └── product/
│ ├── repository/ # Data access layer
│ │ ├── mysql/
│ │ └── postgres/
│ └── service/ # Service layer
├── pkg/ # Public libraries
├── migrations/ # Database migrations
├── static/ # Static assets
├── templates/ # HTML templates
├── go.mod # Go module file
└── README.md # Documentation
This structure follows some common Go project layout conventions:
cmd/
- Contains the main application entry pointsinternal/
- Contains code that shouldn't be imported by other applicationspkg/
- Contains code that can be used by other applications- Separation of concerns between handlers, business logic, and data access
Understanding Each Layer
Let's explore each layer in more detail:
1. API Layer (Handlers and Routes)
The API layer is responsible for handling HTTP requests and responses. It contains:
- Handlers: Process HTTP requests, call appropriate services, and format responses
- Routes: Define the application's endpoints
- Middlewares: Custom request processing that happens before/after handlers
Example routes file (internal/api/routes/user_routes.go
):
package routes
import (
"github.com/labstack/echo/v4"
"my-echo-app/internal/api/handlers"
"my-echo-app/internal/api/middlewares"
)
// RegisterUserRoutes registers all user-related routes
func RegisterUserRoutes(e *echo.Echo, handlers *handlers.UserHandler) {
// User group
userGroup := e.Group("/users")
// Apply middlewares to the group
userGroup.Use(middlewares.AuthMiddleware)
// Routes
userGroup.GET("", handlers.ListUsers)
userGroup.POST("", handlers.CreateUser)
userGroup.GET("/:id", handlers.GetUser)
userGroup.PUT("/:id", handlers.UpdateUser)
userGroup.DELETE("/:id", handlers.DeleteUser)
}
2. Domain Layer
The domain layer contains your business entities and logic. It should be independent of the web framework and database technology.
Example user domain (internal/domain/user/user.go
):
package user
import (
"time"
)
// User represents a user in the system
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // Don't expose in JSON responses
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Validate validates user data
func (u *User) Validate() error {
// Validation logic here
return nil
}
3. Repository Layer
The repository layer handles data access and is responsible for communicating with databases or external services.
Example user repository (internal/repository/postgres/user_repository.go
):
package postgres
import (
"context"
"database/sql"
"my-echo-app/internal/domain/user"
)
// UserRepository handles user database operations
type UserRepository struct {
db *sql.DB
}
// NewUserRepository creates a new user repository
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db}
}
// GetByID fetches a user by their ID
func (r *UserRepository) GetByID(ctx context.Context, id uint) (*user.User, error) {
// Database query logic here
query := "SELECT id, username, email, created_at, updated_at FROM users WHERE id = $1"
// Implementation here...
return &user.User{}, nil
}
// List returns all users with pagination
func (r *UserRepository) List(ctx context.Context, page, limit int) ([]*user.User, error) {
// Implementation here...
return []*user.User{}, nil
}
// More repository methods...
4. Service Layer
The service layer contains business logic and acts as a mediator between the repository and API layers.
Example user service (internal/service/user_service.go
):
package service
import (
"context"
"my-echo-app/internal/domain/user"
"my-echo-app/internal/repository"
)
// UserService handles user-related business logic
type UserService struct {
repo repository.UserRepository
}
// NewUserService creates a new user service
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo}
}
// GetUser retrieves a user by their ID
func (s *UserService) GetUser(ctx context.Context, id uint) (*user.User, error) {
return s.repo.GetByID(ctx, id)
}
// ListUsers retrieves users with pagination
func (s *UserService) ListUsers(ctx context.Context, page, limit int) ([]*user.User, error) {
return s.repo.List(ctx, page, limit)
}
// CreateUser creates a new user
func (s *UserService) CreateUser(ctx context.Context, user *user.User) error {
// Validate user data
if err := user.Validate(); err != nil {
return err
}
// Additional business logic here
// Persist to database
return s.repo.Create(ctx, user)
}
// More service methods...
Real-World Application Structure
Let's look at a complete example of how everything fits together in a real-world application:
Main Application Entry Point
// cmd/api/main.go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"my-echo-app/internal/api/routes"
"my-echo-app/internal/config"
"my-echo-app/internal/repository/postgres"
"my-echo-app/internal/api/handlers"
"my-echo-app/internal/service"
)
func main() {
// Load configuration
cfg := config.Load()
// Connect to database
db, err := postgres.NewConnection(cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
// Initialize repositories
userRepo := postgres.NewUserRepository(db)
// Initialize services
userService := service.NewUserService(userRepo)
// Initialize handlers
userHandler := handlers.NewUserHandler(userService)
// Create Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Register routes
routes.RegisterUserRoutes(e, userHandler)
// Start server
go func() {
if err := e.Start(":" + cfg.ServerPort); err != nil {
e.Logger.Infof("Shutting down the server: %v", err)
}
}()
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
}
Handler Implementation
// internal/api/handlers/user_handler.go
package handlers
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"my-echo-app/internal/domain/user"
"my-echo-app/internal/service"
)
// UserHandler handles HTTP requests related to users
type UserHandler struct {
userService *service.UserService
}
// NewUserHandler creates a new user handler
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService}
}
// GetUser handles GET /users/:id
func (h *UserHandler) GetUser(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid user ID"})
}
user, err := h.userService.GetUser(c.Request().Context(), uint(id))
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, user)
}
// ListUsers handles GET /users
func (h *UserHandler) ListUsers(c echo.Context) error {
page, _ := strconv.Atoi(c.QueryParam("page"))
if page <= 0 {
page = 1
}
limit, _ := strconv.Atoi(c.QueryParam("limit"))
if limit <= 0 || limit > 100 {
limit = 10 // Default limit
}
users, err := h.userService.ListUsers(c.Request().Context(), page, limit)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, users)
}
// CreateUser handles POST /users
func (h *UserHandler) CreateUser(c echo.Context) error {
u := new(user.User)
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request payload"})
}
if err := h.userService.CreateUser(c.Request().Context(), u); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, u)
}
// UpdateUser and DeleteUser methods would follow the same pattern...
Best Practices for Echo Project Structure
- Separation of concerns: Keep your code modular by separating it into distinct layers.
- Dependency injection: Pass dependencies to your handlers, services, and repositories rather than creating them inside.
- Interface-driven design: Define interfaces for your services and repositories to make testing easier.
- Configuration management: Use environment variables or configuration files for settings that might change.
- Consistent error handling: Create a standard error handling approach across your application.
- Middleware organization: Keep middlewares in a separate package for better organization.
Testing Your Echo Application
Your project structure should also accommodate tests. Here's a recommended approach:
my-echo-app/
├── internal/
│ ├── api/
│ │ ├── handlers/
│ │ │ ├── user_handler.go
│ │ │ └── user_handler_test.go // Test for handlers
...
Example test for a handler:
// internal/api/handlers/user_handler_test.go
package handlers
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"my-echo-app/internal/domain/user"
"my-echo-app/internal/service"
)
// Mock user service
type mockUserService struct {
mock.Mock
}
func (m *mockUserService) GetUser(ctx context.Context, id uint) (*user.User, error) {
args := m.Called(ctx, id)
return args.Get(0).(*user.User), args.Error(1)
}
// More mock methods...
func TestGetUser(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/users/:id")
c.SetParamNames("id")
c.SetParamValues("1")
mockService := new(mockUserService)
mockService.On("GetUser", mock.Anything, uint(1)).Return(&user.User{
ID: 1,
Username: "testuser",
Email: "[email protected]",
}, nil)
h := &UserHandler{userService: mockService}
// Assertions
if assert.NoError(t, h.GetUser(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "testuser")
}
mockService.AssertExpectations(t)
}
Summary
A well-structured Echo application helps you maintain clean code, makes collaboration easier, and positions your application for growth. The key components of a good Echo project structure are:
- Clear separation between different layers of your application
- Organized routing and handler functions
- Business logic separated from HTTP concerns
- Database operations isolated in repositories
- Configuration management
- Consistent error handling
Remember that the structure we've discussed is a recommended approach, but Echo is flexible enough to accommodate different organizational patterns based on your specific needs. As your application grows, you may need to adjust the structure to maintain code quality and developer productivity.
Additional Resources
- Echo Framework Documentation
- Standard Go Project Layout
- Clean Architecture in Go
- Go Project Structure Best Practices
Exercises
- Create a basic Echo application with the simple project structure described above.
- Refactor an existing Echo application to use the layered architecture approach.
- Add middleware for authentication and authorization to your Echo application.
- Implement repository interfaces and create both a real database implementation and a mock implementation for testing.
- Create unit tests for your handlers using the mock services pattern shown in the testing example.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)