Skip to content

Routing & Handlers

In Express a handler receives (req, res, next). In Echo every handler has a single, uniform signature:

func(c echo.Context) error

echo.Context bundles the request, response writer, path parameters, query parameters, and helper methods. You return nil on success or an error to let Echo handle the failure response.

TypeScript
// Express handler
app.get('/books/:id', (req, res, next) => {
const id = req.params.id;
const book = findBook(id);
if (!book) return res.status(404).json({ message: 'not found' });
res.json(book);
});
Go
// Echo handler
func getBook(c echo.Context) error {
id := c.Param("id")
book, err := findBook(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "not found")
}
return c.JSON(http.StatusOK, book)
}

Echo’s routing API mirrors Express closely. Methods map directly to HTTP verbs.

TypeScript
// Express
app.get('/books', listBooks);
app.post('/books', createBook);
app.get('/books/:id', getBook);
app.put('/books/:id', updateBook);
app.delete('/books/:id', deleteBook);
Go
// Echo
e.GET("/books", listBooks)
e.POST("/books", createBook)
e.GET("/books/:id", getBook)
e.PUT("/books/:id", updateBook)
e.DELETE("/books/:id", deleteBook)

c.Param("name") returns the string value of a named segment. The colon syntax :name is identical to Express.

TypeScript
// Express
app.get('/users/:id/posts/:postId', (req, res) => {
const { id, postId } = req.params;
res.json({ userId: id, postId });
});
Go
// Echo
e.GET("/users/:id/posts/:postId", func(c echo.Context) error {
userID := c.Param("id")
postID := c.Param("postId")
return c.JSON(http.StatusOK, map[string]string{
"userId": userID,
"postId": postID,
})
})

c.QueryParam("name") reads a single query value. c.QueryParams() returns all of them as url.Values (a map[string][]string).

TypeScript
// Express
app.get('/books', (req, res) => {
const { page = '1', limit = '10' } = req.query;
res.json({ page, limit });
});
Go
// Echo
e.GET("/books", func(c echo.Context) error {
page := c.QueryParam("page")
limit := c.QueryParam("limit")
if page == "" { page = "1" }
if limit == "" { limit = "10" }
return c.JSON(http.StatusOK, map[string]string{
"page": page, "limit": limit,
})
})

Attaching handlers to a struct (NestJS controller style)

Section titled “Attaching handlers to a struct (NestJS controller style)”

NestJS controllers are classes. In Go the idiomatic equivalent is a struct whose methods are the handlers. Dependency injection happens through the struct’s fields.

TypeScript
// NestJS controller
@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.booksService.findOne(id);
}
}
Go
// Echo — handler struct (manual DI)
type BookHandler struct {
store BookStore
}
func NewBookHandler(s BookStore) *BookHandler {
return &BookHandler{store: s}
}
func (h *BookHandler) Get(c echo.Context) error {
id := c.Param("id")
book, err := h.store.FindByID(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
return c.JSON(http.StatusOK, book)
}
// Registration in main.go:
// bh := NewBookHandler(store)
// e.GET("/books/:id", bh.Get)

Run this locally — it needs the Echo module and a network port.

What is the return type of an Echo handler?
How do you read a path parameter named "id" in Echo?
How do you read a query string value named "page" in Echo?
What is the idiomatic Go equivalent of a NestJS controller class?