Getting Started with Building Web Servers Using Go × Echo: Learn Simple, High‑Performance API Development in the Shortest Time
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.
The code created this time is available in the repository below.
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
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
+ 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.
+ 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.
+ 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.
+ 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.
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.
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.
+ 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.
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.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Complete Cache Strategy Guide: Maximizing Performance with CDN, Redis, and API Optimization
2024/03/07Go × Gin Advanced: Practical Techniques and Scalable API Design
2023/11/29Go × Gin Basics: A Thorough Guide to Building a High-speed API Server
2023/11/23Article & Like API with Go + Gin + GORM (Part 1): First, an "implementation-first controller" with all-in-one CRUD
2025/07/13Building an MVC-Structured Blog Post Web API with Go × Gin: From Basics to Scalable Design
2023/12/03Robust Test Design and Implementation Guide in a Go × Gin × MVC Architecture
2023/12/04Bringing a Go + Gin App Up to Production Quality: From Configuration and Structure to CI
2023/12/06Released a string slice utility for Go: “strlistutils”
2025/06/19


