This episode dissects the Echo routing system: colon path parameters, wildcards, regex-based matching, query parameters, route groups, and virtual hosts for serving multiple domains from a single server.

Routing is Echo's main muscle. The radix tree router we discussed in episode 2 will now work with various route patterns: parameters, wildcards, regex, query strings, and virtual hosts. Understanding these patterns correctly will prevent the route conflict errors that often trip up beginner developers.
Episode 4 dissects the Echo routing system thoroughly: colon path parameters, wildcards, regex-based matching, query parameters, route groups with subrouters, and multiple hosts with e.Vhost.
Path parameters let a single route serve many values. Use a colon to define a parameter, then retrieve its value with c.Param:
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.String(http.StatusOK, "user id: "+id)
})The route /users/:id — declared via e.GET("/users/:id", handler) — matches /users/42 and /users/arman. The value after /users/ is retrieved with c.Param("id"). Note that :id doesn't match a slash — a parameter only binds a single path segment.
You can write several parameters in a single route. The router maps each segment to its corresponding name:
e.GET("/users/:id/posts/:postId", func(c echo.Context) error {
userID := c.Param("id")
postID := c.Param("postId")
return c.String(http.StatusOK, userID+" "+postID)
})When a request to /users/7/posts/99 comes in, c.Param("id") returns 7 and c.Param("postId") returns 99. Parameter names are free-form, as long as they're consistent between the route declaration and retrieval.
Sometimes you need to match the rest of a path whose length is unpredictable. The /* wildcard captures all segments after it, including slashes:
e.GET("/files/*", func(c echo.Context) error {
path := c.Param("*")
return c.String(http.StatusOK, "file path: "+path)
})The /* wildcard is a catch-all: the route /files/* matches /files/a, /files/a/b, and so on. c.Param("*") returns the part of the path after /files/.
For tighter control, Echo supports regex constraints on parameters. The pattern :id\\d+ only matches one or more digits:
e.GET("/users/:id\\d+", func(c echo.Context) error {
return c.String(http.StatusOK, "numeric id: "+c.Param("id"))
})This route only accepts numeric ids; a request like /users/abc won't match and will fall through to another route or produce a 404. Regex uses standard Go syntax, so any pattern valid in regexp can be used.
Besides the path, information can come through the query string. c.QueryParam retrieves a single value, while c.QueryParams returns all key-value pairs:
e.GET("/search", func(c echo.Context) error {
query := c.QueryParam("q")
page := c.QueryParam("page")
return c.JSON(http.StatusOK, map[string]string{
"query": query,
"page": page,
})
})A request GET /search?q=echo&page=2 will produce a response with the q and page values. Note that query parameters are always optional — if not sent, the value is an empty string.
curl "http://localhost:8080/search?q=echo&page=2"Route groups bundle routes that share a prefix and middleware. This is the standard pattern for API versions or specific modules:
api := e.Group("/api/v1")
api.Use(middleware.CORS())
api.GET("/users", listUsers)
api.POST("/users", createUser)All routes in the group automatically get the /api/v1 prefix and run the CORS middleware. For routes that don't fit any pattern, e.Any registers a handler for all HTTP methods:
e.Any("/webhook", handleWebhook)When a single server must serve multiple domains, Echo provides e.Vhost. Each vhost contains its own echo.Echo with its own router and middleware:
a := e.Vhost("api.example.com")
a.GET("/", handleAPI)
b := e.Vhost("www.example.com")
b.Use(middleware.Gzip())
b.GET("/", handleWeb)A request with the Host: api.example.com header is routed to vhost a, while www.example.com is handled by vhost b. This is useful for monolithic architectures serving multiple domains in a single process.
A conflict occurs when two routes have the same pattern and priority. A classic example: /users/:id and /users/:name — both match the same format, so Echo rejects the second registration.
e.Debug = true
go run .Set e.Debug = true and run the server, and every request will print debugging information. Also use e.Routes() to print all registered routes at boot time. You'll use this technique again when troubleshooting in episode 19.
Episode 4 made you master the Echo routing system: path parameters with :id, the /* wildcard for the rest of the path, regex :id\\d+ for strict constraints, query parameters, route groups with subrouters, and virtual hosts for multiple domains in a single server.
Key takeaways:
:id matches a single path segment, retrieved via c.Param./* wildcard matches the entire rest of the path.:id\\d+ restricts parameter values.c.QueryParam and c.QueryParams read the query string.e.Group wraps routes with a shared prefix and middleware.e.Any registers a handler for all HTTP methods.e.Vhost serves multiple domains with separate routers.In the next episode we'll cover binding & validation — turning JSON, XML, or form data into structs with c.Bind, struct tags for query and URI binding, custom binders, the go-playground pluggable validator, and centralized 400 error handling.