Skip to main content

Echo Dynamic Routes

Introduction

Dynamic routes are a powerful feature in the Echo framework that allows you to create flexible URL patterns with variable path parameters. Unlike static routes that match exact paths, dynamic routes can capture parts of the URL as variables, making them essential for building RESTful APIs and modern web applications.

In this tutorial, you'll learn how to define dynamic routes in Echo, extract path parameters, and implement handlers that respond to variable URL patterns.

What are Dynamic Routes?

Dynamic routes contain one or more parameters that can vary with each request. These parameters are specified in the URL path using a colon (:) prefix followed by a parameter name.

For example, in the route /users/:id, the :id part is a dynamic parameter that can match any value in that position of the URL path.

Creating Basic Dynamic Routes

Let's start with a simple example of defining and using dynamic routes in Echo:

go
package main

import (
"net/http"

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

func main() {
e := echo.New()

// Dynamic route with a single parameter
e.GET("/users/:id", getUserHandler)

e.Start(":8080")
}

func getUserHandler(c echo.Context) error {
// Extract the 'id' parameter from the URL
id := c.Param("id")

return c.JSON(http.StatusOK, map[string]string{
"message": "Fetching user information",
"userId": id,
})
}

In this example:

  • We defined a route /users/:id where :id is a dynamic parameter
  • The getUserHandler function extracts the value of the id parameter using c.Param("id")
  • The handler returns a JSON response that includes the captured parameter

Input and Output

When you make a request to /users/123, the server responds with:

json
{
"message": "Fetching user information",
"userId": "123"
}

If you request /users/john, the response changes to:

json
{
"message": "Fetching user information",
"userId": "john"
}

Multiple Path Parameters

You can include multiple parameters in a single route pattern:

go
e.GET("/products/:category/:id", func(c echo.Context) error {
category := c.Param("category")
id := c.Param("id")

return c.JSON(http.StatusOK, map[string]string{
"category": category,
"productId": id,
})
})

With this route, a request to /products/electronics/laptop-15 would extract:

  • category = "electronics"
  • id = "laptop-15"

Parameter Validation with Regular Expressions

Echo also supports parameter validation using regular expressions. You can restrict what a parameter can match by adding a regex pattern after the parameter name:

go
// Only match numeric IDs with exactly 5 digits
e.GET("/users/:id^[0-9]{5}$", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"userId": id,
})
})

In this example, the route will only match paths like /users/12345, but not /users/123 (too short) or /users/abc12 (contains letters).

Optional Parameters and Wildcards

Echo provides additional ways to handle variable paths:

Optional Segments

You can make a part of your route optional by using the * notation:

go
// Match both "/files" and "/files/documents/report.pdf"
e.GET("/files/*", func(c echo.Context) error {
path := c.Param("*")
if path == "" {
return c.String(http.StatusOK, "Listing all files")
}
return c.String(http.StatusOK, "Accessing file at: "+path)
})

Catch-All Parameters

The * can also be used as a catch-all parameter to match any number of path segments:

go
e.GET("/assets/*filepath", func(c echo.Context) error {
// The "*filepath" parameter will capture the entire path after "/assets/"
filepath := c.Param("filepath")
return c.String(http.StatusOK, "You requested: "+filepath)
})

A request to /assets/css/style.css would extract filepath as "css/style.css".

Practical Example: RESTful API

Here's a more complete example showing how dynamic routes can be used to build a RESTful API:

go
package main

import (
"fmt"
"net/http"
"strconv"

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

// Simulated database
var users = map[string]User{
"1": {ID: "1", Name: "John", Email: "[email protected]"},
"2": {ID: "2", Name: "Alice", Email: "[email protected]"},
}

type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

func main() {
e := echo.New()

// RESTful routes for users resource
e.GET("/users", getAllUsers)
e.GET("/users/:id", getUser)
e.POST("/users", createUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)

// Nested resources
e.GET("/users/:userId/posts/:postId", getUserPost)

e.Start(":8080")
}

func getAllUsers(c echo.Context) error {
return c.JSON(http.StatusOK, users)
}

func getUser(c echo.Context) error {
id := c.Param("id")
user, exists := users[id]

if !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": fmt.Sprintf("User with ID %s not found", id),
})
}

return c.JSON(http.StatusOK, user)
}

func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return err
}

// Generate ID (simplified)
u.ID = strconv.Itoa(len(users) + 1)
users[u.ID] = *u

return c.JSON(http.StatusCreated, u)
}

func updateUser(c echo.Context) error {
id := c.Param("id")

if _, exists := users[id]; !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": fmt.Sprintf("User with ID %s not found", id),
})
}

u := new(User)
if err := c.Bind(u); err != nil {
return err
}

u.ID = id
users[id] = *u

return c.JSON(http.StatusOK, u)
}

func deleteUser(c echo.Context) error {
id := c.Param("id")

if _, exists := users[id]; !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": fmt.Sprintf("User with ID %s not found", id),
})
}

delete(users, id)
return c.NoContent(http.StatusNoContent)
}

func getUserPost(c echo.Context) error {
userId := c.Param("userId")
postId := c.Param("postId")

return c.JSON(http.StatusOK, map[string]string{
"userId": userId,
"postId": postId,
"message": "Post details retrieved",
})
}

This example demonstrates how dynamic routes help create a clean API structure where resources and sub-resources can be accessed in a predictable, hierarchical way.

Route Priority

When you define multiple routes that could potentially match the same URL, Echo follows specific rules to determine which route handler should process the request:

  1. Static routes have higher priority than dynamic routes
  2. Dynamic routes with more static parts have priority over more generic ones
  3. Catch-all routes have the lowest priority

For example, if you define these routes:

go
e.GET("/users/admin", adminHandler)
e.GET("/users/:id", userHandler)
e.GET("/*", catchAllHandler)
  • A request to /users/admin matches the first route
  • A request to /users/123 matches the second route
  • A request to /products matches the third route

Best Practices for Dynamic Routes

  1. Use semantic parameter names - Choose descriptive names like :userId instead of just :id when your application has multiple ID types

  2. Keep URLs RESTful - Follow REST conventions for resource naming and URL structure

  3. Validate parameters - Always validate path parameters before using them in your application logic

  4. Handle not found cases - Provide appropriate error responses when resources don't exist

  5. Consider case sensitivity - URL paths in Echo are case-sensitive by default

Summary

Dynamic routes are a key feature in Echo that enable flexible URL patterns by capturing variable parts of the path as parameters. This allows you to create clean, intuitive, and RESTful API designs.

In this tutorial, we covered:

  • Creating basic dynamic routes with parameters
  • Extracting parameter values in handlers
  • Using multiple parameters in a single route
  • Parameter validation with regular expressions
  • Optional segments and catch-all parameters
  • Building RESTful APIs with dynamic routes
  • Route priority and best practices

With these techniques, you can build sophisticated web applications that respond intelligently to a variety of URL patterns while keeping your code organized and maintainable.

Further Resources

Exercises

  1. Create a route that handles product information with both category and ID parameters (e.g., /products/:category/:id)

  2. Implement a file server route that uses a catch-all parameter to serve files from a specific directory

  3. Build a simple blog API with routes for posts, comments, and categories using appropriate dynamic routes

  4. Add parameter validation to ensure that ID parameters only accept numeric values

  5. Create a route hierarchy for a nested resource structure (e.g., users → posts → comments)



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)