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:
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
Output:
Hello, World!
The String()
method takes two parameters:
- HTTP status code (
http.StatusOK
equals 200) - The text string to return
HTML Responses
For returning HTML content:
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:
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:
{
"name": "John Doe",
"email": "[email protected]"
}
XML Responses
For XML responses:
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:
<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:
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 succeeded201 Created
: Resource created successfully400 Bad Request
: Invalid request401 Unauthorized
: Authentication required403 Forbidden
: Authenticated but not authorized404 Not Found
: Resource doesn't exist500 Internal Server Error
: Server error
Sending Files
Echo provides methods for sending files:
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:
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
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:
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:
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:
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:
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:
- Different response types (JSON, status codes)
- Error handling with appropriate status codes
- RESTful patterns for resource management
- 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
-
Create an Echo handler that returns different response formats (JSON, XML, or HTML) based on a query parameter
?format=json|xml|html
. -
Implement a file download endpoint that serves a PDF file with a custom filename.
-
Create a streaming endpoint that sends real-time updates (like simulated stock prices) every few seconds.
-
Enhance the task API example to include validation and more detailed error messages.
-
Implement pagination for the task list endpoint, accepting
page
andlimit
query parameters.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)