Purpose of this article #
In this article I want to give you a taste of what building Go web applications with Echo framework looks like. It is hard and time consuming to go through the entire documentation of a framework to just get the feel of what it is like and if the framework is for you.
We will use the example of a Todo app for demonstration covering topics like routing, middleware, authentication, path/query parameters, request body and more. The complete example application can be found in this github repository.
Why a todo app? #
I have seen a lot of hate ๐ towards todo apps being used as an example in tutorials and courses. Todo apps are a great way to showcase the technology/framework to the reader without requiring the reader to gain additional context of the project. When you read "todo app" you already know the what the requirements are. Additionaly todo apps cover the most common operations of framework/libraries which is CRUD (create, read, update, delete) data.
Project Setup #
Create a new project using go mod init <your_module_name>
and install the echo framework.
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
Setting up a Data Store #
You need a way to store/manage the todos, you will not be using a database as that is not the focus of this article, you will be storing the data in-memory.
Create file in the main package itself called todomanager.go
and paste the following code in it. We have something called a TodoManager
, we will be adding all the CRUD operations as functions on this TodoManager
so give this code a good read.
package main import ( "sync" ) // Todo represents our todo type Todo struct { ID string `json:"id"` Title string `json:"title"` IsComplete bool `json:"isDone"` } type TodoManager struct { todos []Todo m sync.Mutex // we want all our operations to be atomic } func NewTodoManager() TodoManager { return TodoManager{ todos: make([]Todo, 0), m: sync.Mutex{}, } }
todomanager.go
Setting up Echo #
Now you are going to set up a simple http server using echo
.
package main import ( "github.com/labstack/echo/v4" ) func main() { e := echo.New() e.Start(":8888") }
main.go
Run the code using go run main.go
. This will start an http server on port 8888
and you should see the following output in your terminal.
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.9.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
โจ http server started on [::]:8888
Building the app #
Let's start adding functionality to the app. The steps will be quite similar for all of the operations: 1. Add a function in TodoManager
, 2. Add an api endpoint in echo.
The code snippets below will only showcase new additions and leave out the already written code, if you feel lost or want to see the enitre working thing head over to the this GitHub repo that contains the complete working example.
Get all the todos #
Inside TodoManager
add a function to return all todos.
... func (tm *TodoManager) GetAll() []Todo { return tm.todos }
todomanager.go
Add a GET
route on echo.
package main import ( "net/http" "github.com/labstack/echo/v4" ) func main() { tm := NewTodoManager() // ๐ instance of todo manager e := echo.New() // ๐ new GET route e.GET("/", func(c echo.Context) error { todos := tm.GetAll() return c.JSON(http.StatusOK, todos) // ๐ sending json data back }) e.Start(":8888") }
main.go
It is as simple as using the GET
function on echo and providing it with a route path and a function.
Note: The function signature has a single parameter of type echo.Context
, it is not your standard func(http.ResponseWriter, *http.Request)
signature.
Let's send a curl request and see the output.
โฏ curl -s localhost:8888/ | jq
[]
As expected you will see an empty array because there is no todo yet ๐คทโโ๏ธ.
Middleware to log all requests #
Echo has the concept of middlewares which are functions that run before the actual request function.
Let's set up a middleware to log all the requests to terminal.
You will be using an inbuilt middleware provided by echo (you will see an example of a custom middleware later) from its github.com/labstack/echo/v4/middleware
package.
package main import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main() { tm := NewTodoManager() e := echo.New() e.Use(middleware.Logger()) // ๐ log all requests e.GET("/", func(c echo.Context) error { todos := tm.GetAll() return c.JSON(http.StatusOK, todos) }) e.Start(":8888") }
main.go
If you send a request again you should see a json object being printed in the standard output.
{"time":"2022-09-08T04:07:37.20349-04:00","id":"","remote_ip":"127.0.0.1","host":"localhost:8888","method":"GET","uri":"/","user_agent":"curl/7.79.1","status":200,"error":"","latency":72063,"latency_human":"72.063ยตs","bytes_in":0,"bytes_out":3}
Echo has a number of built in middlewares, check out the documentation on middleware here.
Authenticated routes #
Before we proceed adding additional routes in our application, let's talk about authentication. I am not going to discuss how to do authentication (not the focus of this article). I am going to show you how to make routes accessible only by logged in user.
โญ๏ธ A common pattern in backend applications is to pass an auth token as a header in every request, Authorization: <auth_token>
.
You will use Echo Groups to group together routes that require authentication and add a custom middleware that will check for the presence of an auth token, for simplicity it will be a simple string equality check.
func main() { ... authenticatedGroup := e.Group("/todos", func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // ๐ check for auth token authorization := c.Request().Header.Get("authorization") if authorization != "auth-token" { c.Error(echo.ErrUnauthorized) return nil } // ๐ token exists, keep moving forward next(c) return nil } }) ... }
main.go
Any routes added onto authenticatedGroup
will have the middleware executed before the actual route is executed.
In this middleware, there is a check for authentication
header, if the header is empty or not valid an error is returned using echo's c.Error
.
Sending a request without a header will give you back this response.
โฏ curl -s localhost:8888/todos/ | jq
{
"message": "Unauthorized"
}
Create new todo #
Using the previous authenticated group add a route to create a new todo.
Let's take care of the TodoManager
part by adding a Create
function.
// ๐ required data to create new todo type CreateTodoRequest struct { Title string `json:"title"` } func (tm *TodoManager) Create(createTodoRequest CreateTodoRequest) Todo { tm.m.Lock() defer tm.m.Unlock() newTodo := Todo{ ID: strconv.FormatInt(time.Now().UnixMilli(), 10), Title: createTodoRequest.Title, IsComplete: false, } tm.todos = append(tm.todos, newTodo) return newTodo }
todomanager.go
Now set up a POST
route which will parse the request body and call the TodoManager.Create
function.
func main() { ... authenticatedGroup.POST("/create", func(c echo.Context) error { requestBody := CreateTodoRequest{} // ๐ parses the body and "binds" it to the passed type err := c.Bind(&requestBody) if err != nil { return err } todo := tm.Create(requestBody) return c.JSON(http.StatusCreated, todo) }) ... }
main.go
Since this create route is on the group that requires authentication you need to pass the auth token. Making the request using curl
, you get back a new todo.
โฏ curl -s -X POST localhost:8888/todos/create \
-H "Content-Type: application/json" \
-H "Authorization: auth-token" \
-d '{ "title": "Todo Title" }' \
| jq
{
"id": "1662719050096",
"title": "Todo Title",
"isDone": false
}
Fetch all the todos using the GET /
endpoint.
โฏ curl -s localhost:8888/ | jq
[
{
"id": "1662719050096",
"title": "Todo Title",
"isDone": false
}
]
Complete a todo #
Now you will add a new PATCH /todos/:id/complete
endpoint that marks a todo as complete. Here you will see how to parse path parameters in echo.
First add a new function Complete(ID string)
in the TodoManager
.
func (tm *TodoManager) Complete(ID string) error { tm.m.Lock() defer tm.m.Unlock() // Find the todo with id var todo *Todo var index int = -1 for i, t := range tm.todos { if t.ID == ID { todo = &t index = i } } if todo == nil { return echo.ErrNotFound } // Check todo is not already completed if todo.IsComplete { err := echo.ErrBadRequest err.Message = "todo is already complete" return err } // Update todo tm.todos[index].IsComplete = true return nil }
todomanager.go
Notice that the errors being returned are from echo itself, echo.ErrNotFound
and echo.ErrBadRequest
. If these errors are passed to echo's e.Error
function, echo will automatically set the appropriate status code and message in the response.
func main() { ... authenticatedGroup.PATCH("/:id/complete", func(c echo.Context) error { id := c.Param("id") // ๐ getting the path parameter err := tm.Complete(id) if err != nil { c.Error(err) return err } return nil }) ... }
main.go
The value path parameter (id
) is being grabbed by using c.Param
function. There is a similar function in echo called c.QueryParam
to get values of query parameters.
โฏ curl -i -X PATCH localhost:8888/todos/1662757576599/complete \
-H "Content-Type: application/json" \
-H "Authorization: auth-token"
HTTP/1.1 200 OK
Date: Fri, 09 Sep 2022 21:06:25 GMT
Content-Length: 0
Deleting a todo #
Add a function in TodoManager
to remove a todo.
func (tm *TodoManager) Remove(ID string) error { tm.m.Lock() defer tm.m.Unlock() index := -1 for i, t := range tm.todos { if t.ID == ID { index = i break } } if index == -1 { return echo.ErrNotFound } tm.todos = append(tm.todos[:index], tm.todos[index+1:]...) return nil }
todomanager.go
Add a route on authenticated group to delete a the todo.
func main() { ... authenticatedGroup.DELETE("/:id", func(c echo.Context) error { id := c.Param("id") err := tm.Remove(id) if err != nil { c.Error(err) return err } return nil }) ... }
main.go
Just like the complete todo endpoint you are parsing the path parameter using c.Param
.
โฏ curl -i -X POST localhost:8888/todos/1662763466488 \
-H "Content-Type: application/json" \
-H "Authorization: auth-token"
HTTP/1.1 200 OK
Date: Fri, 09 Sep 2022 22:44:33 GMT
Content-Length: 0
I hope this article gave you a good overview of echo framework, there is so much more you can do with echo, if you are interested visit the echo's official page here.