Echo Response Formatting
When developing an Echo API, proper response formatting is crucial for ensuring your API is easy to consume, predictable, and professional. This guide will walk you through the fundamentals of response formatting with practical examples that you can implement right away.
Introduction to Response Formatting
Response formatting refers to how your API structures and delivers data back to the client. A well-formatted response makes your API:
- Easier to integrate with
- More consistent across endpoints
- More maintainable over time
- Clearer when communicating errors or success states
In Echo framework, we have multiple ways to format and return responses. Let's explore the most common approaches and best practices.
Basic Response Types in Echo
JSON Responses
JSON (JavaScript Object Notation) is the most common format for API responses due to its widespread support and readability.
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
func main() {
e := echo.New()
e.GET("/hello", func(c echo.Context) error {
// Return JSON response
return c.JSON(http.StatusOK, map[string]string{
"message": "Hello, World!",
})
})
e.Start(":8080")
}
When you call this endpoint, you'll receive:
{
"message": "Hello, World!"
}
Other Response Types
Echo supports various other response types:
// String response
e.GET("/string", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// HTML response
e.GET("/html", func(c echo.Context) error {
return c.HTML(http.StatusOK, "<h1>Hello, World!</h1>")
})
// Stream response
e.GET("/stream", func(c echo.Context) error {
return c.Stream(http.StatusOK, "application/octet-stream", reader)
})
Standardizing Your API Responses
Creating a Response Structure
It's a good practice to standardize your API responses with a consistent structure. Here's a common pattern:
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Meta interface{} `json:"meta,omitempty"`
}
func sendResponse(c echo.Context, statusCode int, success bool, data interface{}, err string) error {
return c.JSON(statusCode, Response{
Success: success,
Data: data,
Error: err,
})
}
Now you can use this helper function in your handlers:
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
user, err := getUserById(id)
if err != nil {
return sendResponse(c, http.StatusNotFound, false, nil, "User not found")
}
return sendResponse(c, http.StatusOK, true, user, "")
})
The response would look like:
{
"success": true,
"data": {
"id": "123",
"name": "John Doe",
"email": "[email protected]"
}
}
Or in case of an error:
{
"success": false,
"error": "User not found"
}
Handling Different Status Codes
HTTP status codes are important for communicating the result of an API call. Here's how to handle common scenarios:
Successful Responses
// 200 OK - The request was successful
return c.JSON(http.StatusOK, data)
// 201 Created - A new resource was created
return c.JSON(http.StatusCreated, newResource)
// 204 No Content - Successful but no content to return
return c.NoContent(http.StatusNoContent)
Error Responses
// 400 Bad Request - Invalid input
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid input"})
// 401 Unauthorized - Authentication required
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Authentication required"})
// 403 Forbidden - Not allowed to access
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied"})
// 404 Not Found - Resource not found
return c.JSON(http.StatusNotFound, map[string]string{"error": "Resource not found"})
// 500 Internal Server Error - Server error
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Internal server error"})
Pagination and Metadata
For endpoints that return collections, it's helpful to include pagination information:
type PaginatedResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Meta Pagination `json:"meta"`
}
type Pagination struct {
Total int `json:"total"`
PerPage int `json:"per_page"`
CurrentPage int `json:"current_page"`
LastPage int `json:"last_page"`
}
e.GET("/users", func(c echo.Context) error {
page, _ := strconv.Atoi(c.QueryParam("page"))
if page < 1 {
page = 1
}
perPage := 10
users, total := getUsers(page, perPage)
lastPage := (total + perPage - 1) / perPage
return c.JSON(http.StatusOK, PaginatedResponse{
Success: true,
Data: users,
Meta: Pagination{
Total: total,
PerPage: perPage,
CurrentPage: page,
LastPage: lastPage,
},
})
})
The response would look like:
{
"success": true,
"data": [
{ "id": "1", "name": "User 1" },
{ "id": "2", "name": "User 2" }
],
"meta": {
"total": 57,
"per_page": 10,
"current_page": 1,
"last_page": 6
}
}
Error Handling Best Practices
Consistent Error Objects
Create a standard error response structure:
type ErrorResponse struct {
Success bool `json:"success"`
Error ErrorDetails `json:"error"`
}
type ErrorDetails struct {
Code string `json:"code"`
Message string `json:"message"`
Field string `json:"field,omitempty"`
}
func handleError(c echo.Context, status int, code, message, field string) error {
return c.JSON(status, ErrorResponse{
Success: false,
Error: ErrorDetails{
Code: code,
Message: message,
Field: field,
},
})
}
Usage example:
e.POST("/users", func(c echo.Context) error {
var user User
if err := c.Bind(&user); err != nil {
return handleError(c, http.StatusBadRequest, "INVALID_INPUT",
"Failed to parse request body", "")
}
if user.Email == "" {
return handleError(c, http.StatusBadRequest, "MISSING_FIELD",
"Email is required", "email")
}
// Process the user...
return c.JSON(http.StatusCreated, map[string]interface{}{
"success": true,
"data": user,
})
})
Validation Errors
For form validation, it's useful to return multiple errors:
type ValidationResponse struct {
Success bool `json:"success"`
Errors []ValidationError `json:"errors"`
}
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
e.POST("/register", func(c echo.Context) error {
var user User
if err := c.Bind(&user); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid input"})
}
var errors []ValidationError
if len(user.Username) < 3 {
errors = append(errors, ValidationError{
Field: "username",
Message: "Username must be at least 3 characters",
})
}
if len(user.Password) < 8 {
errors = append(errors, ValidationError{
Field: "password",
Message: "Password must be at least 8 characters",
})
}
if len(errors) > 0 {
return c.JSON(http.StatusBadRequest, ValidationResponse{
Success: false,
Errors: errors,
})
}
// Continue processing...
return c.JSON(http.StatusCreated, map[string]interface{}{
"success": true,
"data": map[string]string{
"id": "new-user-id",
"message": "User registered successfully",
},
})
})
Real-world Application: RESTful API
Let's build a more complete example of a RESTful API for a blog with proper response formatting:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"strconv"
"time"
)
type (
Post struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Meta interface{} `json:"meta,omitempty"`
}
)
// Mock database
var posts = []Post{
{1, "First Post", "This is my first post", time.Now().Add(-48 * time.Hour)},
{2, "Echo Framework", "Echo is awesome", time.Now().Add(-24 * time.Hour)},
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/posts", getAllPosts)
e.GET("/posts/:id", getPost)
e.POST("/posts", createPost)
e.Start(":8080")
}
func getAllPosts(c echo.Context) error {
return c.JSON(http.StatusOK, Response{
Success: true,
Data: posts,
Meta: map[string]interface{}{
"total": len(posts),
},
})
}
func getPost(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, Response{
Success: false,
Error: "Invalid post ID",
})
}
for _, post := range posts {
if post.ID == id {
return c.JSON(http.StatusOK, Response{
Success: true,
Data: post,
})
}
}
return c.JSON(http.StatusNotFound, Response{
Success: false,
Error: "Post not found",
})
}
func createPost(c echo.Context) error {
post := new(Post)
if err := c.Bind(post); err != nil {
return c.JSON(http.StatusBadRequest, Response{
Success: false,
Error: "Invalid input data",
})
}
// Validate
if post.Title == "" || post.Content == "" {
return c.JSON(http.StatusBadRequest, Response{
Success: false,
Error: "Title and content are required",
})
}
// Set new post ID and creation time
post.ID = len(posts) + 1
post.CreatedAt = time.Now()
// Add to "database"
posts = append(posts, *post)
return c.JSON(http.StatusCreated, Response{
Success: true,
Data: post,
})
}
Content Negotiation
Echo allows clients to request different response formats using the Accept header:
e.GET("/data", func(c echo.Context) error {
data := map[string]string{"message": "Hello, World!"}
switch c.Request().Header.Get("Accept") {
case "application/xml":
return c.XML(http.StatusOK, data)
default:
return c.JSON(http.StatusOK, data)
}
})
Summary
Proper response formatting is essential for creating consistent, maintainable, and user-friendly APIs. By standardizing your response structure, handling errors appropriately, and including necessary metadata, you can ensure your Echo API is both robust and easy to use.
Key takeaways:
- Use consistent response structures with fields like
success
,data
, anderror
- Always return appropriate HTTP status codes
- Include pagination information for collection endpoints
- Provide detailed error messages with specific codes
- Consider versioning your API responses for backward compatibility
Additional Resources
Exercises
- Create an Echo API that returns a user profile with appropriate success/error responses
- Implement pagination for a collection of items
- Build a standard error handler middleware that formats all errors consistently
- Create a simple CRUD API for a resource with proper response formatting for all operations
- Implement content negotiation to support both JSON and XML responses
By mastering response formatting in Echo, you'll create APIs that are a pleasure to use and integrate with, saving time and reducing frustration for both you and your API consumers.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)