Echo MVC Pattern
Introduction
The Model-View-Controller (MVC) pattern is a software architectural design that separates an application into three main components: Models, Views, and Controllers. While Echo doesn't enforce MVC like some frameworks do, it's perfectly suited for implementing this pattern to create well-organized, maintainable web applications.
In this guide, we'll explore how to structure an Echo application using the MVC pattern, why it's beneficial, and how to implement it step by step.
What is MVC?
Before diving into implementation details, let's understand the core components of MVC:
- Model: Handles data logic and business rules
- View: Manages the presentation layer (what users see)
- Controller: Acts as an intermediary between Model and View, handling user input and routing
Using MVC with Echo helps you achieve:
- Better code organization
- Improved maintainability
- Easier collaboration among team members
- Cleaner separation of concerns
Implementing MVC in Echo
Let's build a simple task management application using the MVC pattern with Echo.
Project Structure
First, let's establish a sensible directory structure:
taskapp/
├── main.go
├── controllers/
│ └── task_controller.go
├── models/
│ └── task.go
├── views/
│ └── tasks.html
└── routes/
└── routes.go
Creating the Model
Let's start with our task model:
// models/task.go
package models
import (
"time"
)
// Task represents a task in our application
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
}
// TaskStore manages task data
type TaskStore struct {
tasks []Task
nextID int
}
// NewTaskStore creates a new task store
func NewTaskStore() *TaskStore {
return &TaskStore{
tasks: make([]Task, 0),
nextID: 1,
}
}
// GetAll returns all tasks
func (ts *TaskStore) GetAll() []Task {
return ts.tasks
}
// Add creates a new task
func (ts *TaskStore) Add(title, description string) Task {
task := Task{
ID: ts.nextID,
Title: title,
Description: description,
Completed: false,
CreatedAt: time.Now(),
}
ts.tasks = append(ts.tasks, task)
ts.nextID++
return task
}
// GetByID retrieves a task by ID
func (ts *TaskStore) GetByID(id int) (Task, bool) {
for _, task := range ts.tasks {
if task.ID == id {
return task, true
}
}
return Task{}, false
}
// ToggleCompleted toggles the completion status of a task
func (ts *TaskStore) ToggleCompleted(id int) bool {
for i, task := range ts.tasks {
if task.ID == id {
ts.tasks[i].Completed = !task.Completed
return true
}
}
return false
}
Creating the Controller
Now let's implement the controller to handle HTTP requests:
// controllers/task_controller.go
package controllers
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"yourproject/models"
)
// TaskController manages task-related operations
type TaskController struct {
store *models.TaskStore
}
// NewTaskController creates a new task controller
func NewTaskController(store *models.TaskStore) *TaskController {
return &TaskController{store: store}
}
// GetTasks returns all tasks
func (tc *TaskController) GetTasks(c echo.Context) error {
tasks := tc.store.GetAll()
return c.JSON(http.StatusOK, tasks)
}
// GetTask returns a single task by ID
func (tc *TaskController) GetTask(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid ID"})
}
task, found := tc.store.GetByID(id)
if !found {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Task not found"})
}
return c.JSON(http.StatusOK, task)
}
// CreateTask creates a new task
func (tc *TaskController) CreateTask(c echo.Context) error {
type request struct {
Title string `json:"title"`
Description string `json:"description"`
}
req := new(request)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
}
task := tc.store.Add(req.Title, req.Description)
return c.JSON(http.StatusCreated, task)
}
// ToggleTaskStatus toggles the completion status of a task
func (tc *TaskController) ToggleTaskStatus(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid ID"})
}
success := tc.store.ToggleCompleted(id)
if !success {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Task not found"})
}
task, _ := tc.store.GetByID(id)
return c.JSON(http.StatusOK, task)
}
Setting up Routes
Let's organize our routes:
// routes/routes.go
package routes
import (
"github.com/labstack/echo/v4"
"yourproject/controllers"
"yourproject/models"
)
// Setup configures all routes for the application
func Setup(e *echo.Echo) {
// Create model store
taskStore := models.NewTaskStore()
// Create controller
taskController := controllers.NewTaskController(taskStore)
// API routes
api := e.Group("/api")
{
tasks := api.Group("/tasks")
{
tasks.GET("", taskController.GetTasks)
tasks.GET("/:id", taskController.GetTask)
tasks.POST("", taskController.CreateTask)
tasks.PUT("/:id/toggle", taskController.ToggleTaskStatus)
}
}
}
Adding Views (HTML Templates)
For a complete MVC implementation, let's add a simple HTML template:
<!-- views/tasks.html -->
<!DOCTYPE html>
<html>
<head>
<title>Task Manager</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.task {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 10px;
border-radius: 5px;
}
.completed {
background-color: #f8f8f8;
text-decoration: line-through;
}
.task-form {
margin-bottom: 20px;
}
input, textarea {
width: 100%;
padding: 8px;
margin-bottom: 10px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Task Manager</h1>
<div class="task-form">
<h2>Add New Task</h2>
<input type="text" id="title" placeholder="Title">
<textarea id="description" placeholder="Description"></textarea>
<button onclick="createTask()">Add Task</button>
</div>
<h2>Tasks</h2>
<div id="tasks-container"></div>
<script>
// Fetch and display tasks
async function loadTasks() {
const response = await fetch('/api/tasks');
const tasks = await response.json();
const container = document.getElementById('tasks-container');
container.innerHTML = '';
tasks.forEach(task => {
const taskElement = document.createElement('div');
taskElement.className = `task ${task.completed ? 'completed' : ''}`;
taskElement.innerHTML = `
`;
container.appendChild(taskElement);
});
}
// Create a new task
async function createTask() {
const title = document.getElementById('title').value;
const description = document.getElementById('description').value;
if (!title) {
alert('Title is required');
return;
}
await fetch('/api/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, description })
});
document.getElementById('title').value = '';
document.getElementById('description').value = '';
loadTasks();
}
// Toggle task completion status
async function toggleTask(id) {
await fetch(`/api/tasks/${id}/toggle`, {
method: 'PUT'
});
loadTasks();
}
// Load tasks on page load
document.addEventListener('DOMContentLoaded', loadTasks);
</script>
</body>
</html>
Updating our Controller to Serve HTML
Let's add a method to serve our HTML view:
// Add this to controllers/task_controller.go
// RenderTasksPage renders the tasks HTML page
func (tc *TaskController) RenderTasksPage(c echo.Context) error {
return c.File("views/tasks.html")
}
And update our routes:
// Add this to routes/routes.go, inside Setup function
// Web routes
e.GET("/", taskController.RenderTasksPage)
Main Application
Finally, let's tie everything together in our main application:
// main.go
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"yourproject/routes"
)
func main() {
// Create Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Setup routes
routes.Setup(e)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
Example: Running the Application
When you run this application and navigate to http://localhost:8080/
, you'll see a task management interface where you can:
- Create new tasks
- View all tasks
- Toggle task completion status
Input-Output Example
Let's walk through creating a task:
Input: When a user submits this JSON via POST to /api/tasks
:
{
"title": "Learn Echo MVC Pattern",
"description": "Study the MVC pattern implementation in Echo framework"
}
Output: The server responds with:
{
"id": 1,
"title": "Learn Echo MVC Pattern",
"description": "Study the MVC pattern implementation in Echo framework",
"completed": false,
"created_at": "2023-06-10T15:30:45.123Z"
}
Best Practices for MVC in Echo
To make the most of the MVC pattern in Echo:
- Keep controllers thin: Controllers should only handle HTTP-related logic, not business logic
- Use services for complex logic: For complex applications, consider adding a service layer between controllers and models
- Organize routes by resource: Group routes logically by the resources they manage
- Use dependency injection: Pass dependencies to controllers rather than creating them internally
- Standard naming conventions: Use consistent naming patterns for your controllers and methods
Real-World Application Example
Let's expand our task application with a more realistic implementation:
Adding Database Integration
// models/task.go - updated with database
package models
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql"
)
// TaskStore manages task data with database
type TaskStore struct {
db *sql.DB
}
// NewTaskStore creates a new task store
func NewTaskStore(dsn string) (*TaskStore, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// Create task table if it doesn't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
completed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return nil, err
}
return &TaskStore{db: db}, nil
}
// GetAll returns all tasks
func (ts *TaskStore) GetAll() ([]Task, error) {
rows, err := ts.db.Query("SELECT id, title, description, completed, created_at FROM tasks")
if err != nil {
return nil, err
}
defer rows.Close()
var tasks []Task
for rows.Next() {
var t Task
if err := rows.Scan(&t.ID, &t.Title, &t.Description, &t.Completed, &t.CreatedAt); err != nil {
return nil, err
}
tasks = append(tasks, t)
}
return tasks, nil
}
// Add creates a new task
func (ts *TaskStore) Add(title, description string) (Task, error) {
result, err := ts.db.Exec(
"INSERT INTO tasks (title, description) VALUES (?, ?)",
title, description,
)
if err != nil {
return Task{}, err
}
id, err := result.LastInsertId()
if err != nil {
return Task{}, err
}
return Task{
ID: int(id),
Title: title,
Description: description,
Completed: false,
CreatedAt: time.Now(),
}, nil
}
// GetByID retrieves a task by ID
func (ts *TaskStore) GetByID(id int) (Task, bool, error) {
var t Task
err := ts.db.QueryRow(
"SELECT id, title, description, completed, created_at FROM tasks WHERE id = ?",
id,
).Scan(&t.ID, &t.Title, &t.Description, &t.Completed, &t.CreatedAt)
if err == sql.ErrNoRows {
return Task{}, false, nil
} else if err != nil {
return Task{}, false, err
}
return t, true, nil
}
// ToggleCompleted toggles the completion status of a task
func (ts *TaskStore) ToggleCompleted(id int) (bool, error) {
result, err := ts.db.Exec(
"UPDATE tasks SET completed = NOT completed WHERE id = ?",
id,
)
if err != nil {
return false, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return false, err
}
return rowsAffected > 0, nil
}
Error Handling Middleware
// middleware/error_handler.go
package middleware
import (
"github.com/labstack/echo/v4"
"net/http"
)
type ErrorResponse struct {
Status int `json:"status"`
Message string `json:"message"`
}
// ErrorHandler is a middleware that handles errors
func ErrorHandler(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
if err != nil {
c.Logger().Error(err)
var statusCode int
var message string
switch e := err.(type) {
case *echo.HTTPError:
statusCode = e.Code
message = e.Message.(string)
default:
statusCode = http.StatusInternalServerError
message = "Internal Server Error"
}
if !c.Response().Committed {
return c.JSON(statusCode, ErrorResponse{
Status: statusCode,
Message: message,
})
}
}
return nil
}
}
Updated Main Application
// main.go
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log"
"os"
customMiddleware "yourproject/middleware"
"yourproject/models"
"yourproject/routes"
)
func main() {
// Get database DSN from environment variable
dsn := os.Getenv("DATABASE_DSN")
if dsn == "" {
dsn = "user:password@tcp(localhost:3306)/taskdb?parseTime=true"
}
// Create task store with database connection
taskStore, err := models.NewTaskStore(dsn)
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
// Create Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(customMiddleware.ErrorHandler)
// Setup routes with the taskStore
routes.Setup(e, taskStore)
// Start server
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
e.Logger.Fatal(e.Start(":" + port))
}
Summary
The MVC pattern in Echo provides a structured approach to organizing your web applications. By separating concerns into distinct components:
- Models handle data storage and business logic
- Views manage the presentation layer
- Controllers coordinate between models and views, handling HTTP requests
This separation makes your code more maintainable, testable, and extensible. While Echo doesn't enforce MVC, its flexible design makes implementing this pattern straightforward and effective.
Additional Resources
Exercises
- Extend the task application to include user authentication
- Add categorization functionality to tasks
- Implement a REST API with full CRUD operations
- Create a search feature for tasks
- Add pagination to the task list
By completing these exercises, you'll gain a deeper understanding of how the MVC pattern can be implemented in Echo applications and develop practical skills for building well-structured web applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)