Skip to main content

Routing

Arrower uses Echo for routing. No custom abstraction, so the full Echo API is available. This page covers Arrower-specific conventions and patterns. For everything else, refer to the Echo documentation.

Route Groups

The DI container provides three preconfigured routers:

RouterDescriptionUsage
WebRouterPublic-facing web routesGET /login, POST /register
AdminRouterAdmin dashboard routes, protected by defaultGET /admin/users
APIRouterREST API routesGET /api/v1/...
func (c *MyContext) registerWebRoutes(router *echo.Group) {
router.GET("/hello", c.controller.SayHello())
}

Controller Naming Convention

Arrower follows a consistent naming pattern across all contexts:

ActionHTTP MethodPathController Method
IndexGET/usersIndex()
ShowGET/users/:idShow()
CreateGET/users/createCreate()
StorePOST/usersStore()
EditGET/users/:id/editEdit()
UpdatePUT/PATCH/users/:idUpdate()
DeleteDELETE/users/:idDelete()

Each controller method returns func(c echo.Context) error:

func (ctrl *UserController) Show() func(c echo.Context) error {
return func(c echo.Context) error {
id := c.Param("id")
// ...
return c.Render(http.StatusOK, "users.show", data)
}
}

Named Routes

Give routes a name to resolve URLs dynamically in templates:

router.GET("/login", c.userController.Login()).Name = "auth.login"
router.GET("/users/:userID", c.userController.Show()).Name = "admin.users.show"

Named routes work with the route template helper in your HTML views. No need to hardcode URLs - change the route path in one place and all links update automatically:

<a href="{{ route "auth.login" }}">Login</a>
<a href="{{ route "admin.users.show" userID }}">View User</a>

Binding & Validation

Arrower uses Echo's built-in binding and validation with go-playground/validator:

func (ctrl *UserController) Store() func(c echo.Context) error {
type createUserRequest struct {
Email string `form:"email" validate:"required,email"`
Password string `form:"password" validate:"required,min=8"`
PasswordConfirm string `form:"password_confirm" validate:"required,eqfield=Password"`
}

return func(c echo.Context) error {
var req createUserRequest

if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "could not parse request").WithInternal(err)
}

if err := c.Validate(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid input").WithInternal(err)
}

// process req...
}
}

Protected Routes

Auth middleware is provided for route-level access control:

// Protect a single route
router.GET("/profile", c.userController.Profile(), auth.EnsureUserIsLoggedInMiddleware)

// Protect all routes in a group
adminRouter.Use(auth.EnsureUserIsSuperuserMiddleware)

See Auth for all available middleware.

Admin Routes

The AdminRouter is a route group mounted at /admin, created by the DI container. Each context contributes its own admin sub-routes without knowing about other contexts. The admin dashboard ties them together with a shared sidebar navigation.

// DI container creates the admin group
dc.AdminRouter = dc.WebRouter.Group("/admin")

// Each context registers its own sub-group
adminRouter := di.AdminRouter.Group("/auth") // → /admin/auth/*
adminRouter := di.AdminRouter.Group("/jobs") // → /admin/jobs/*

Contexts register admin routes during initialisation:

func (c *AuthContext) registerAdminRoutes(router *echo.Group) {
router.GET("/users", c.userController.List()).Name = "admin.users"
router.GET("/users/new", c.userController.New())
router.POST("/users/new", c.userController.Store())
router.GET("/settings", c.settingsController.List()).Name = "admin.users.settings"
}

The whole /admin group is protected with the superuser middleware (see Protected Routes). Route names use the admin.* prefix so they can be resolved in the shared admin layout:

<a href="{{ route "admin.users" }}">Users</a>
<a href="{{ route "admin.jobs" }}">Job Queues</a>

The admin dashboard uses a shared admin layout with sidebar navigation. Contexts only provide the page content - it is inserted into the existing layout automatically. Developers do not control the surrounding layout, only the page template. See Views - Template Hierarchy for details on how layouts work.

Rendering

Controllers render templates using Echo's Render method (see Views for all rendering options):

return c.Render(http.StatusOK, "jobs.index", echo.Map{
"jobs": jobs,
})

Template names follow the context.action pattern (e.g. auth.login, jobs.index, admin.users). See Views for details on templates and the renderer.