Time to write real code: creating your first Echo application with echo.New, installing the slog-based RequestLogger and Recover middleware, writing handlers that return text and JSON, and running the server on port 8080 with proper error handling.

All the foundations are ready: the Go environment, Echo v5, and an understanding of the architecture. Now comes the part you've been waiting for — writing your first Echo application. In this episode you'll experience first-hand how a REST server rises from zero in less than 50 lines of code.
Episode 3 builds a healthy starter project: echo.New() as the entry point, the RequestLogger middleware with slog and Recover, two first handlers that return text and JSON, and how to run the server on port 8080 with proper error handling.
Every Echo application starts with echo.New(). This function creates an echo.Echo instance complete with a router, default binder, and built-in configuration. This instance is what you configure with middleware, routes, and a validator.
package main
import (
"net/http"
"github.com/labstack/echo/v5"
)
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"))
}Save it as main.go and run it:
go run .Open another terminal and test it:
curl http://localhost:8080/If everything works, you'll see Hello, Echo! on your screen. Notice the pattern e.Logger.Fatal(e.Start(":8080")): e.Start blocks the process until the server stops, and the error only appears if the server fails to start.
A healthy Echo application never runs without logging. In Echo v5, middleware.RequestLogger uses Go's standard log/slog — one of the major updates that didn't exist in v4. Configure the log fields that are actually useful:
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogMethod: true,
LogURIPath: true,
LogStatus: true,
LogLatency: true,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
slog.Info("request",
"method", v.Method,
"uri", v.URIPath,
"status", v.Status,
"latency", v.Latency.String(),
)
return nil
},
}))This middleware logs the method, path, status, and latency of every request. With slog, logs are produced in a structured format ready for machine processing — something you'll take full advantage of in episodes 11 and 18.
A panicking handler must not stop the server. middleware.Recover catches panics, turns them into a 500 error response, and logs the stack trace so the server stays alive:
e.Use(middleware.Recover())Attach Recover right after RequestLogger — this order ensures panics are still logged by the logger before being handled.
Handlers are the heart of the application. Echo provides helpers for every response type. The two most basic ones: c.String for plain text and c.JSON for structured data.
e.GET("/ping", func(c echo.Context) error {
return c.String(http.StatusOK, "pong")
})
type Health struct {
Status string `json:"status"`
Service string `json:"service"`
}
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, Health{
Status: "ok",
Service: "belajar-echo",
})
})In the code above, c.JSON serializes a struct into JSON with the help of the json struct tags. The resulting response looks like {"status":"ok","service":"belajar-echo"}.
Notice one important habit: handlers return errors rather than writing them out themselves. This is Echo's centralized error handling philosophy. When binding or business logic fails, the handler just does return err and the framework decides the final response.
e.GET("/error", func(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "bad request")
})echo.NewHTTPError creates a centralized error with a status code and message. The full details of customizing error responses will be covered in episode 11.
e.Start(":8080") runs the server on port 8080. Note the :8080 format without a host — Echo will listen on all interfaces, both IPv4 and IPv6. When port 8080 is busy, e.Start returns an error and you can see it through e.Logger.Fatal.
Test several methods at once to get a feel for how the router handles routes:
curl http://localhost:8080/ping
curl http://localhost:8080/health
curl -i http://localhost:8080/tidak-adaA request to /tidak-ada will produce a 404. This is the default HTTPError managed by the router when no route matches.
The application you built now has three layers that will grow throughout the series:
RequestLogger for logging, Recover for stability./, /ping, /health, and /error.gofmt -w main.go
go vet ./...Get into the habit of running gofmt -w main.go and go vet before every commit. Clean, verified code will make all the following episodes easier.
Episode 3 marked the start of writing real code: your first Echo application with echo.New(), the slog-based RequestLogger and Recover middleware, handlers that return text and JSON, the pattern of returning errors from handlers, and a server running on port 8080.
Key takeaways:
echo.New().e.Start(":8080") runs the server; errors are wrapped by e.Logger.Fatal.RequestLogger in v5 uses structured log/slog.Recover turns panics into 500 responses without killing the server.c.String for text, c.JSON for structured data.In the next episode we'll cover routing & URL parameters — path parameters with :id, wildcards, regex-based matching, query parameters, route groups, and virtual hosts. This is where the radix tree router really starts to show its strengths.