Skip to main content

Echo Response Handling

Introduction

Response handling is a fundamental concept in web development that determines how your application communicates back to clients. In the Echo framework, response handling enables you to control what data is sent back, in what format, and with what status code. Mastering response handling is essential for building robust web applications and APIs that communicate effectively with clients.

In this guide, we'll explore the various ways Echo allows you to handle responses, from simple text responses to structured JSON, and how to handle errors appropriately.

Basic Response Types

Echo provides several methods to return different types of responses. Let's explore the most common ones:

Text Responses

The simplest type of response is plain text:

go
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

Output:

Hello, World!

The String() method takes two parameters:

  1. HTTP status code (http.StatusOK equals 200)
  2. The text string to return

HTML Responses

For returning HTML content:

go
e.GET("/html", func(c echo.Context) error {
return c.HTML(http.StatusOK, "<h1>Hello, World!</h1>")
})

Output (rendered in browser):

Hello, World!

JSON Responses

JSON is widely used for API responses:

go
e.GET("/user", func(c echo.Context) error {
user := struct {
Name string `json:"name"`
Email string `json:"email"`
}{
Name: "John Doe",
Email: "[email protected]",
}
return c.JSON(http.StatusOK, user)
})

Output:

json
{
"name": "John Doe",
"email": "[email protected]"
}

XML Responses

For XML responses:

go
e.GET("/data", func(c echo.Context) error {
type Data struct {
XMLName xml.Name `xml:"data"`
Name string `xml:"name"`
Value string `xml:"value"`
}
d := &Data{
Name: "temperature",
Value: "72°F",
}
return c.XML(http.StatusOK, d)
})

Output:

xml
<data>
<name>temperature</name>
<value>72°F</value>
</data>

Working with Status Codes

HTTP status codes communicate the result of a client's request. Echo makes it easy to send appropriate status codes:

go
e.GET("/not-found", func(c echo.Context) error {
return c.String(http.StatusNotFound, "Resource not found")
})

e.GET("/unauthorized", func(c echo.Context) error {
return c.String(http.StatusUnauthorized, "Please login to access this resource")
})

e.GET("/server-error", func(c echo.Context) error {
return c.String(http.StatusInternalServerError, "Something went wrong on our end")
})

Common HTTP status codes:

  • 200 OK: Request succeeded
  • 201 Created: Resource created successfully
  • 400 Bad Request: Invalid request
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Authenticated but not authorized
  • 404 Not Found: Resource doesn't exist
  • 500 Internal Server Error: Server error

Sending Files

Echo provides methods for sending files:

go
e.GET("/file", func(c echo.Context) error {
return c.File("path/to/file.pdf")
})

e.GET("/attachment", func(c echo.Context) error {
return c.Attachment("path/to/report.pdf", "quarterly-report.pdf")
})

The Attachment() method prompts the browser to download the file with the provided filename.

Streaming Responses

For large responses or real-time data, you can use streaming:

go
e.GET("/stream", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, "text/event-stream")
c.Response().Header().Set(echo.HeaderCacheControl, "no-cache")
c.Response().WriteHeader(http.StatusOK)

for i := 0; i < 10; i++ {
if _, err := c.Response().Write([]byte(fmt.Sprintf("data: Message %d\n\n", i))); err != nil {
return err
}
c.Response().Flush()
time.Sleep(1 * time.Second)
}

return nil
})

Error Handling

Proper error handling is critical for robust applications. Echo makes this straightforward:

Basic Error Handling

go
e.GET("/error-example", func(c echo.Context) error {
// Simulate an error condition
if err := someOperation(); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to process request")
}
return c.String(http.StatusOK, "Operation successful")
})

Custom Error Responses

You can customize error responses by using HTTP errors:

go
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
user, err := findUser(id)
if err != nil {
if err == ErrUserNotFound {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return echo.NewHTTPError(http.StatusInternalServerError, "Database error")
}
return c.JSON(http.StatusOK, user)
})

Global Error Handler

You can also implement a global error handler:

go
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal Server Error"

if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = fmt.Sprintf("%v", he.Message)
}

// Don't send error for client disconnects
if c.Response().Committed || c.Request().Context().Err() == context.Canceled {
return
}

if c.Request().Method == http.MethodHead {
c.NoContent(code)
return
}

if c.Request().Header.Get(echo.HeaderAccept) == "application/json" {
c.JSON(code, map[string]string{"error": message})
return
}

c.HTML(code, fmt.Sprintf("<h1>Error</h1><p>%s</p>", message))
}

Content Negotiation

Content negotiation allows your API to serve different representations of the same resource based on client preferences:

go
e.GET("/data", func(c echo.Context) error {
data := struct {
Message string `json:"message" xml:"message"`
Count int `json:"count" xml:"count"`
}{
Message: "Hello, World!",
Count: 42,
}

switch c.Request().Header.Get(echo.HeaderAccept) {
case "application/xml":
return c.XML(http.StatusOK, data)
default:
return c.JSON(http.StatusOK, data)
}
})

Practical Example: RESTful API

Let's build a simple RESTful API for managing tasks:

go
package main

import (
"net/http"
"strconv"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
}

var tasks = []Task{
{ID: 1, Title: "Learn Echo", Description: "Learn Echo framework basics", Completed: false},
{ID: 2, Title: "Build API", Description: "Create a RESTful API with Echo", Completed: false},
}

func main() {
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Routes
e.GET("/tasks", getAllTasks)
e.GET("/tasks/:id", getTask)
e.POST("/tasks", createTask)
e.PUT("/tasks/:id", updateTask)
e.DELETE("/tasks/:id", deleteTask)

e.Start(":8080")
}

// Get all tasks
func getAllTasks(c echo.Context) error {
return c.JSON(http.StatusOK, tasks)
}

// Get a task by ID
func getTask(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID")
}

for _, task := range tasks {
if task.ID == id {
return c.JSON(http.StatusOK, task)
}
}

return echo.NewHTTPError(http.StatusNotFound, "Task not found")
}

// Create a new task
func createTask(c echo.Context) error {
task := new(Task)
if err := c.Bind(task); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid task data")
}

// Generate a new ID
task.ID = len(tasks) + 1
tasks = append(tasks, *task)

return c.JSON(http.StatusCreated, task)
}

// Update an existing task
func updateTask(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID")
}

task := new(Task)
if err := c.Bind(task); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid task data")
}

for i, t := range tasks {
if t.ID == id {
task.ID = id
tasks[i] = *task
return c.JSON(http.StatusOK, task)
}
}

return echo.NewHTTPError(http.StatusNotFound, "Task not found")
}

// Delete a task
func deleteTask(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID")
}

for i, task := range tasks {
if task.ID == id {
tasks = append(tasks[:i], tasks[i+1:]...)
return c.NoContent(http.StatusNoContent)
}
}

return echo.NewHTTPError(http.StatusNotFound, "Task not found")
}

This example demonstrates:

  1. Different response types (JSON, status codes)
  2. Error handling with appropriate status codes
  3. RESTful patterns for resource management
  4. Using NoContent for successful operations that don't return data

Summary

Effective response handling is crucial for creating usable and robust web applications. Echo provides a rich set of tools for returning various content types, setting appropriate status codes, handling errors, and implementing content negotiation.

Key takeaways:

  • Use appropriate response methods (String(), JSON(), XML(), etc.) for different content types
  • Always include proper HTTP status codes
  • Implement consistent error handling
  • Consider content negotiation for APIs that serve multiple clients
  • Streaming responses are useful for large data sets or real-time data

Additional Resources

Exercises

  1. Create an Echo handler that returns different response formats (JSON, XML, or HTML) based on a query parameter ?format=json|xml|html.

  2. Implement a file download endpoint that serves a PDF file with a custom filename.

  3. Create a streaming endpoint that sends real-time updates (like simulated stock prices) every few seconds.

  4. Enhance the task API example to include validation and more detailed error messages.

  5. Implement pagination for the task list endpoint, accepting page and limit query parameters.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)