Echo API Basics
Introduction
Echo is a high-performance, extensible, and minimalist web framework for Go (Golang). It's designed to make building web applications and APIs straightforward and enjoyable, even for beginners. In this guide, we'll explore the fundamentals of creating APIs using the Echo framework, from setting up your first project to handling different types of requests.
Echo stands out among Go web frameworks because of its simplicity, excellent performance, and middleware support. If you're new to API development, Echo provides an ideal starting point due to its intuitive design and comprehensive documentation.
Getting Started with Echo
Installation
Before we start building APIs with Echo, we need to install the framework. Open your terminal and run:
go get github.com/labstack/echo/v4
This command adds Echo to your Go project dependencies.
Creating Your First Echo Server
Let's create a simple "Hello, World!" API to understand Echo's basic structure:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
// Create a new Echo instance
e := echo.New()
// Route: GET /hello
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// Start the server
e.Start(":8080")
}
Output when you visit http://localhost:8080/hello:
Hello, World!
Let's break down what's happening:
- We import the Echo package
- Create a new Echo instance with
echo.New()
- Define a GET route at the
/hello
path - Provide a handler function that returns a simple string response
- Start the server on port 8080
Understanding Echo Context
The echo.Context
interface is central to Echo applications. It provides access to request and response objects, path parameters, form data, and more. Let's examine a more detailed example:
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Route with path parameter
e.GET("/users/:id", func(c echo.Context) error {
// Get path parameter
id := c.Param("id")
return c.String(http.StatusOK, fmt.Sprintf("User ID: %s", id))
})
// Route with query parameter
e.GET("/search", func(c echo.Context) error {
// Get query parameter
query := c.QueryParam("q")
return c.String(http.StatusOK, fmt.Sprintf("Search query: %s", query))
})
e.Start(":8080")
}
Sample outputs:
- For
/users/123
:User ID: 123
- For
/search?q=golang
:Search query: golang
Handling Different HTTP Methods
RESTful APIs typically use different HTTP methods for different operations. Echo makes it easy to handle these various methods:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Define routes for different HTTP methods
e.GET("/items", getItems)
e.POST("/items", createItem)
e.PUT("/items/:id", updateItem)
e.DELETE("/items/:id", deleteItem)
e.Start(":8080")
}
func getItems(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Fetching all items",
})
}
func createItem(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{
"message": "Item created successfully",
})
}
func updateItem(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"message": "Updated item " + id,
})
}
func deleteItem(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"message": "Deleted item " + id,
})
}
This example demonstrates how to implement a basic CRUD API (Create, Read, Update, Delete) using appropriate HTTP methods.
Working with Request and Response Data
Handling JSON Requests
Most modern APIs exchange data using JSON. Echo makes it simple to work with JSON requests:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
// Define a struct to represent our data
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
e := echo.New()
e.POST("/users", func(c echo.Context) error {
// Create a new user instance
user := new(User)
// Bind the request body to the user struct
if err := c.Bind(user); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request payload",
})
}
// Process the user data (in a real app, you might save to a database)
// Return the created user with a 201 Created status
return c.JSON(http.StatusCreated, user)
})
e.Start(":8080")
}
Sample Input:
{
"name": "John Doe",
"email": "[email protected]",
"age": 30
}
Sample Output:
{
"name": "John Doe",
"email": "[email protected]",
"age": 30
}
The c.Bind()
method automatically binds the request data to the provided struct, mapping JSON fields to struct fields.
Form Data and File Uploads
Echo can also handle form submissions and file uploads:
package main
import (
"fmt"
"io"
"net/http"
"os"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.POST("/upload", func(c echo.Context) error {
// Get form fields
name := c.FormValue("name")
// Get file from request
file, err := c.FormFile("file")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "File upload failed",
})
}
// Open the source file
src, err := file.Open()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Cannot open uploaded file",
})
}
defer src.Close()
// Create destination file
dst, err := os.Create(file.Filename)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Cannot create destination file",
})
}
defer dst.Close()
// Copy source file to destination
if _, err = io.Copy(dst, src); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Cannot save file",
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": fmt.Sprintf("File %s uploaded successfully for %s", file.Filename, name),
})
})
e.Start(":8080")
}
Middleware in Echo
Middleware functions in Echo process requests before they reach your route handlers. They're perfect for tasks like logging, authentication, and request validation.
Creating a Simple Logging Middleware
package main
import (
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
)
// LoggerMiddleware logs request details
func LoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Record start time
start := time.Now()
// Log request information
fmt.Printf("Request: %s %s\n", c.Request().Method, c.Request().URL.Path)
// Call the next handler
err := next(c)
// Calculate response time
duration := time.Since(start)
// Log response information
fmt.Printf("Response: %d (%s)\n", c.Response().Status, duration)
return err
}
}
func main() {
e := echo.New()
// Apply middleware to all routes
e.Use(LoggerMiddleware)
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Start(":8080")
}
When you visit /hello
, your console will display logs like:
Request: GET /hello
Response: 200 (2.5ms)
Group Routes and Path Prefixes
For larger APIs, you'll want to organize related routes. Echo's Group feature helps with this:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Create an API group with prefix "/api"
api := e.Group("/api")
// Define routes within the API group
api.GET("/status", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "API is operational",
})
})
// Create a sub-group for user-related endpoints
users := api.Group("/users")
users.GET("", func(c echo.Context) error {
return c.JSON(http.StatusOK, []string{"John", "Jane", "Bob"})
})
users.GET("/:id", func(c echo.Context) error {
id := c.Param("id")
return c.String(http.StatusOK, "User ID: "+id)
})
e.Start(":8080")
}
This creates the following routes:
/api/status
/api/users
/api/users/:id
Real-World Example: Building a Simple Todo API
Let's apply what we've learned to create a simple in-memory Todo API:
package main
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
)
// Todo represents a task in our system
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
// In-memory storage for todos
var todos = []Todo{
{ID: 1, Title: "Learn Echo framework", Completed: false},
{ID: 2, Title: "Build a REST API", Completed: false},
}
func main() {
e := echo.New()
// Get all todos
e.GET("/todos", func(c echo.Context) error {
return c.JSON(http.StatusOK, todos)
})
// Get a specific todo by ID
e.GET("/todos/:id", func(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 format",
})
}
for _, todo := range todos {
if todo.ID == id {
return c.JSON(http.StatusOK, todo)
}
}
return c.JSON(http.StatusNotFound, map[string]string{
"error": "Todo not found",
})
})
// Create a new todo
e.POST("/todos", func(c echo.Context) error {
todo := new(Todo)
if err := c.Bind(todo); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request payload",
})
}
// Generate a new ID (in a real app, this would be handled differently)
todo.ID = len(todos) + 1
// Add to our collection
todos = append(todos, *todo)
return c.JSON(http.StatusCreated, todo)
})
// Update a todo
e.PUT("/todos/:id", func(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 format",
})
}
// Find the todo to update
for i, todo := range todos {
if todo.ID == id {
// Bind new data to the todo
updatedTodo := new(Todo)
if err := c.Bind(updatedTodo); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request payload",
})
}
// Preserve the ID
updatedTodo.ID = id
// Update our collection
todos[i] = *updatedTodo
return c.JSON(http.StatusOK, updatedTodo)
}
}
return c.JSON(http.StatusNotFound, map[string]string{
"error": "Todo not found",
})
})
// Delete a todo
e.DELETE("/todos/:id", func(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 format",
})
}
// Find and remove the todo
for i, todo := range todos {
if todo.ID == id {
// Remove from slice
todos = append(todos[:i], todos[i+1:]...)
return c.JSON(http.StatusOK, map[string]string{
"message": "Todo deleted successfully",
})
}
}
return c.JSON(http.StatusNotFound, map[string]string{
"error": "Todo not found",
})
})
e.Start(":8080")
}
This example implements a complete CRUD API for managing a list of todos.
Error Handling in Echo
Echo provides a customizable error handling system. Here's how to implement custom error responses:
package main
import (
"errors"
"net/http"
"github.com/labstack/echo/v4"
)
// Custom error types
var (
ErrNotFound = errors.New("resource not found")
ErrBadRequest = errors.New("bad request parameters")
)
// Custom HTTP error handler
func customHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal server error"
// Check for specific error types
if err == ErrNotFound {
code = http.StatusNotFound
message = "Resource not found"
} else if err == ErrBadRequest {
code = http.StatusBadRequest
message = "Invalid request parameters"
} else if he, ok := err.(*echo.HTTPError); ok {
// Echo's built-in HTTP errors
code = he.Code
message = he.Message.(string)
}
// Don't log 404s for cleaner logs
if code != http.StatusNotFound {
c.Logger().Error(err)
}
// Send JSON response
c.JSON(code, map[string]string{
"error": message,
})
}
func main() {
e := echo.New()
// Set custom error handler
e.HTTPErrorHandler = customHTTPErrorHandler
e.GET("/item/:id", func(c echo.Context) error {
id := c.Param("id")
// Simulate a resource not found error
if id == "999" {
return ErrNotFound
}
// Simulate a bad request error
if id == "0" {
return ErrBadRequest
}
return c.String(http.StatusOK, "Item found: "+id)
})
e.Start(":8080")
}
This code demonstrates how to create custom errors and a custom error handler to provide meaningful API responses.
Summary
In this guide, we've covered the fundamentals of building APIs with Echo:
- Setting up an Echo server
- Handling different HTTP methods
- Working with route parameters and query strings
- Processing JSON requests and responses
- Implementing middleware
- Organizing routes with groups
- Building a complete CRUD API
- Handling errors gracefully
Echo's straightforward design makes it an excellent choice for beginners while still offering the performance and features needed for production applications. Its middleware system, context-based handlers, and intuitive API help you build clean, maintainable web services.
Additional Resources
- Echo Framework Official Documentation
- Go Programming Language Tour
- RESTful API Design Best Practices
Practice Exercises
-
Basic Echo Server: Create an Echo server that returns your name when accessing the
/me
endpoint. -
Enhanced Todo API: Extend the todo API example to include filtering by completion status (e.g.,
/todos?completed=true
). -
User Authentication: Implement a simple authentication system using Echo middleware that checks for an API key in request headers.
-
Data Validation: Add validation to the Todo API to ensure that new todos have a non-empty title.
-
API Documentation: Use Echo's built-in Swagger integration to document your API endpoints.
As you work through these exercises, remember that the Echo community is active and supportive, with plenty of examples and discussions available online to help you along your learning journey.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)