Getting Started with Building Web Servers Using Go × Echo: Learn Simple, High‑Performance API Development in the Shortest Time

  • echo
    echo
  • golang
    golang
Published on 2023/12/03

Introduction

When you try to build a web application in Go, one of the first things you’ll struggle with is “which framework to choose.”
You can write an HTTP server using only the Go standard library, but if you want convenient features such as routing and middleware, introducing a framework is very effective.

In this article, we’ll use “Echo,” which is known among Go frameworks for being particularly lightweight, fast, and low in learning cost, to explain how to launch a minimally configured web server.

We’ll proceed in the following order:

  • Why choose Echo
  • Installing and initializing Echo
  • Understanding the basic structure and routing
  • How to write request handling and responses
  • How to use middleware

Let’s move forward together with the goal of “first, build something that works” using Go and Echo.
In the next section, we’ll briefly summarize the reasons for choosing Echo.

https://echo.labstack.com/

The code created this time is available in the repository below.

https://github.com/shinagawa-web/go-echo-example

Why Choose Go + Echo

There are multiple web frameworks for Go. Representative ones include Gin, Fiber, Echo, and Chi. The reasons for choosing Echo among them lie in the following characteristics.

1. Outstanding speed and lightness

Echo is known for running extremely fast. It has its own routing engine and achieves high performance with minimal overhead.

2. Simple and intuitive API

It is designed so that error handling, routing, and middleware introduction can be written concisely, making it easy to handle even for beginners.
Another attraction is that the official documentation is extensive, making it easy to gain a consistent understanding from introduction to advanced usage.

3. Rich built‑in middleware and features

  • Logging
  • Recovery (panic handling)
  • Middleware such as CORS, Gzip, JWT

Since these are provided as standard in Echo, you don’t have to spend time searching for and introducing separate libraries.

4. Compatibility with the ecosystem

Echo is built on top of net/http, so it’s easy to use together with other Go libraries and middleware.
It is also designed to integrate naturally with Go’s standard context and error handling, making it easy to extend.

Comparison with Gin (Learning Cost, Features, Syntax)

The web frameworks most often compared in Go are Gin and Echo. Both are fast and popular, but each has strengths and weaknesses depending on your use case and preferences. Here we’ll organize the differences mainly from three perspectives: “learning cost,” “features,” and “syntax.”

Learning cost: Echo is slightly more explicit

Gin has a high level of abstraction and a simple syntax that lets you write compact code, so it’s easy to get started with. However, many internal behaviors are hidden, which can cause stumbling blocks when extending or debugging.

Echo has an explicit structure, with each component separated, which has the advantage that “it’s easy to follow the flow of the framework.” Its style of writing is also close to the Go standard library, making it highly compatible for those who want to learn “Go‑like” coding.

Feature comparison: Echo has richer built‑in features

Item Gin Echo
Routing Fast and flexible Fast with powerful middleware integration
Middleware Minimal (mainly custom) Many included by default (CORS, Gzip, etc.)
Validation Can integrate with validation libraries Built‑in original validation
Response API Easily return JSON, HTML, etc. Supports more diverse formats
Server config Many Gin‑specific settings Faithful to net/http with flexibility

Both are excellent choices, but Echo is designed to be closer to “the Go way of writing,” and many people find it comfortable once they get used to it. In the next chapter, we’ll actually install Echo and set up a project.

Installing and Initializing Echo

To start development with Echo, you’ll set up a standard Go project structure and install Echo. Here we’ll check up to the point of launching a simple server.

1. Create the project directory

mkdir go-echo-example
cd go-echo-example
go mod init go-echo-example

2. Install Echo

$ go get github.com/labstack/echo/v4

3. Minimal main.go

package main

import (
	"net/http"

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

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, Echo!")
	})

	e.Logger.Fatal(e.Start(":8080"))
}

4. Start the server

go run main.go

If you access http://localhost:8080 in your browser, you should see Hello, Echo! displayed.

5. Folder structure (considering future expansion)

In this series, we’ll basically use the following structure:

go-echo-example/
├── go.mod
├── main.go
├── handler/       // HTTP handlers
├── service/       // Business logic layer
├── repository/    // Data access layer
└── model/         // Domain models

By separating directories from the initial stage like this, you can scale the application without strain even as it grows.

Supplement: Enabling hot reload with Air

During development, it’s tedious to manually run go run every time you change the code. To solve this, we’ll introduce air to enable hot reload.

Install air with the following command:

go install github.com/air-verse/air@latest

air can use a configuration file called .air.toml. Create it in the project root with the following content:

# .air.toml
root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -o ./tmp/main ."
  bin = "tmp/main"
  include_ext = ["go"]
  exclude_dir = ["tmp", "vendor"]

[log]
  time = true
Item Meaning
root Root directory to watch
tmp_dir Location to place build artifacts
[build] Build and execution settings
cmd Build command (here, build the Go binary to tmp/main)
bin Path to the binary file to execute
include_ext File extensions to watch
exclude_dir Directories to exclude from watching

Run the following command and air will watch for source code changes and restart immediately.

air

Image from Gyazo

Understanding Echo’s Basic Structure

Echo is characterized by allowing you to build web applications with minimal code. In this section, we’ll look at the overall picture of an Echo app and how it receives requests and returns responses.

Basic structure

A typical structure for an application using Echo looks like this:

package main

import (
	"net/http"

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

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, Echo!")
	})

	e.Start(":8080")
}

Explanation of each element

Component Description
echo.New() Creates an Echo instance. Used for routing and middleware configuration.
e.GET(...) Defines a route handler for an HTTP method and path. You can also use POST, PUT, DELETE, etc.
c echo.Context Context for accessing the request and response. A distinctive Echo interface.
c.String(...) Returns a text response to the client. You can also use c.JSON(), c.HTML(), etc.
e.Start(...) Starts the server and listens for requests on the specified port.

Simple yet powerful routing

Echo’s routing is very intuitive. You can stack route settings like method chains, which makes it especially suitable for small to medium‑sized applications.

Basics of Request Handling

In Echo, obtaining request data is very simple. It supports various input methods such as query parameters, path parameters, and JSON bodies, and you can obtain them through Echo’s unique Context.

Getting query parameters

main.go
+ e.GET("/search", func(c echo.Context) error {
+ 	keyword := c.QueryParam("q")
+ 	return c.String(http.StatusOK, "query: "+keyword)
+ })

Access the following URL:

curl 'http://localhost:8080/search?q=golang'

Response

"query": "golang"

Getting path parameters

To get values embedded in the path, define routing in the form :param.

main.go
+ e.GET("/users/:id", func(c echo.Context) error {
+ 	id := c.Param("id")
+ 	return c.String(http.StatusOK, "user_id: "+id)
+ })

Access the following URL:

curl http://localhost:8080/users/123

Response

"user_id": "123"

Getting form data

Data sent from a form can be obtained with c.FormValue.

main.go
+ e.POST("/login", func(c echo.Context) error {
+ 	username := c.FormValue("username")
+ 	password := c.FormValue("password")
+ 	return c.String(http.StatusOK, "user_name: "+username+", password: "+password)
+ })
curl -X POST -d "name=Gopher&password=p1234567" http://localhost:8080/login

Response

user_name: , password:p1234567

Getting JSON and binding to a struct

When data is sent from the client in JSON format, you can bind it to a struct with c.Bind.

main.go
+ type Message struct {
+ 	Title   string `json:"title"`
+ 	Content string `json:"content"`
+ }

func main() {
	r := gin.Default()



+ e.POST("/messages", func(c echo.Context) error {
+ 	msg := new(Message)
+ 	if err := c.Bind(msg); err != nil {
+ 		return err
+ 	}
+ 	return c.JSON(http.StatusOK, msg)
+ })
}

Send a POST request with curl.

curl -X POST http://localhost:8080/messages \
  -H "Content-Type: application/json" \
  -d '{"title": "Hello", "content": "This is a test message"}'

Response

{"title":"Hello","content":"This is a test message"}

How to Write Responses

Echo can return responses in various formats such as JSON, HTML, and plain text. Here we’ll introduce some representative ways to write responses.

Returning JSON

You can define a struct and return it as JSON.

e.GET("/json", func(c echo.Context) error {
	msg := Message{
		Title:   "Hello",
		Content: "This is a JSON response",
	}
	return c.JSON(http.StatusOK, msg)
})

Access /json with curl.

 curl http://localhost:8080/json

Response

{"title":"Hello","content":"This is a JSON response"}
  • c.JSON(status, obj) encodes obj as JSON and returns it.

Returning HTML

Echo can also return HTML using a template engine, but first here’s a simple way to return an HTML string.

e.GET("/html", func(c echo.Context) error {
	html := `<h1>Welcome</h1><p>This is HTML content.</p>`
	return c.HTML(http.StatusOK, html)
})

Access it in your browser to check.

Image from Gyazo

Using arbitrary status codes

For example, you can return 404 when data is not found.

e.GET("/notfound", func(c echo.Context) error {
	return c.String(http.StatusNotFound, "Not Found")
})

Access /notfound with curl.

curl -i http://localhost:8080/notfound

Response

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Date: Mon, 16 Jun 2025 00:31:15 GMT
Content-Length: 9

Not Found

Using Middleware

Echo has a well‑designed mechanism for easily introducing middleware, allowing you to add frequently used web app features such as logging, recovery (panic handling), CORS, and Basic authentication with “thin code.”

What is middleware?

Middleware is processing that is inserted “between” the request and response. It’s suitable for executing common processing for all requests.

For example:

  • Record request logs
  • Perform authentication/authorization
  • Return a 500 error when a panic occurs
  • Add CORS headers

How to use built‑in middleware

Echo comes with several useful middleware out of the box.

mian.go
import (
	"net/http"

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

e := echo.New()

+ e.Use(middleware.Logger())
+ e.Use(middleware.Recover())



+	e.GET("/panic", func(c echo.Context) error {
+		panic("something went wrong")
+	})

If you access it with curl, logs will be output.

{"time":"2025-06-16T09:46:37.223514+09:00","id":"","remote_ip":"::1","host":"localhost:8080","method":"GET","uri":"/notfound","user_agent":"curl/8.7.1","status":404,"error":"","latency":9333,"latency_human":"9.333µs","bytes_in":0,"bytes_out":9}

Also, when you access /panic, the following message will be output on the client side.

curl http://localhost:8080/panic

Response

{"message":"Internal Server Error"}

Defining custom middleware

You can also define your own middleware.
By inserting processing before and after next like this, you can control the order of processing.

func myMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		// Pre-processing
		println("Before handler")

		// Call the main handler
		err := next(c)

		// Post-processing
		println("After handler")
		return err
	}
}

e.Use(myMiddleware)

You can also apply middleware on a per‑route basis.

main.go
+	g := e.Group("")
+	g.Use(myMiddleware)
-	e.GET("/notfound", func(c echo.Context) error {
+	g.GET("/notfound", func(c echo.Context) error {
		return c.String(http.StatusNotFound, "Not Found")
	})

Access /notfound with curl.

curl -i http://localhost:8080/notfound

You can confirm on the server side that logs are displayed only for specific routes.

Image from Gyazo

Conclusion

In this article, we carefully explained how to build a web server using Go × Echo, from the basic structure to the use of middleware.

Echo is a flexible and high‑speed web framework with a low learning cost. Compared to Gin, one of its attractions is that you can write more intuitive code with a simpler style. It’s suitable not only for beginners but also has enough potential to handle medium‑sized projects.

If this article has helped you grasp even a little of Echo’s style and way of thinking, that’s great. Be sure to incorporate it into your own projects and learning, and deepen your understanding by actually writing code.

In future articles, we plan to cover more practical API design with Echo, as well as testing, configuration management, and deployment methods.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form