Basics

Routing

Define your routes with Echo and HAMR's middleware groups.

HAMR uses Echo v4 under the hood. Routes are registered in internal/web/server.go via a RegisterRoutes function that receives all your dependencies.

Defining a route

internal/web/server.go
func RegisterRoutes(srv *server.Server, deps *Deps) {
    e := srv.Echo()

    homeHandler := home.NewHandler()
    e.GET("/", homeHandler.Index)

    booksHandler := books.NewHandler(deps.BookRepo)
    e.GET("/books", booksHandler.List)
    e.GET("/books/:id", booksHandler.Show)
    e.POST("/books", booksHandler.Create)
}

Route groups

HAMR organizes routes into groups so middleware is applied consistently. A typical setup has three groups:

internal/web/server.go
// Health check — no session middleware.
e.GET("/health", healthHandler.Health)

// Site routes — sessions, CSRF, flash, security headers.
site := e.Group("")
site.Use(middleware.Logging())
site.Use(hamrmw.SecureWithConfig(hamrmw.SecureConfig{
    ContentSecurityPolicy: csp,
}))
site.Use(hamrmw.FlashWithConfig(hamrmw.FlashConfig{Secure: !deps.DevMode}))
site.Use(hamrmw.CSRFWithConfig(hamrmw.CSRFConfig{Secure: !deps.DevMode}))
site.Use(hamrmw.OptionalAuth(authCfg))

site.GET("/", homeHandler.Index)
site.GET("/books", booksHandler.List)
Why per-group, not global?
API routes don't need flash messages or CSRF tokens. Health checks shouldn't run session middleware. Grouping middleware by purpose keeps each request fast and predictable.

Auth and RBAC are per-route

Authentication middleware is applied to individual routes, not to the whole group. This way, looking at a route line tells you exactly what access it requires.

internal/web/server.go
requireAuth := hamrmw.RequireAuth(authCfg)
requireAdmin := hamrmw.RequireRoles(roleChecker, "admin")

// Public routes — no auth.
site.GET("/", homeHandler.Index)
site.GET("/books", booksHandler.List)

// Authenticated routes.
site.GET("/dashboard", dashboardHandler.Index, requireAuth)
site.POST("/books", booksHandler.Create, requireAuth)

// Admin routes.
site.GET("/admin", adminHandler.Index, requireAuth, requireAdmin)

API routes

JSON API routes typically live in their own group with bearer-token auth and CORS instead of session cookies and CSRF.

internal/api/server.go
func RegisterRoutes(srv *server.Server, deps *Deps) {
    e := srv.Echo()

    api := e.Group("/api")
    api.Use(hamrmw.CORSWithConfig(corsConfig))
    api.Use(hamrmw.RateLimit(deps.RateLimitStore))

    api.GET("/health", healthHandler.Health)
    api.GET("/books", booksHandler.List, hamrmw.Auth(authCfg))
}

Static pages

Pages that don't depend on database or session state can be registered as static pages. The dev server pre-generates them as HTML files at build time, and the runtime server still serves them as live routes.

internal/web/server.go
func RegisterStaticPages(srv *server.Server) {
    aboutHandler := about.NewHandler()
    srv.StaticPage("/about", aboutHandler.About)

    docsHandler := docs.NewHandler()
    srv.StaticPage("/docs", docsHandler.Overview)
    srv.StaticPage("/docs/routing", docsHandler.Routing)
}
When to use StaticPage
Use it for marketing pages, documentation, and anything else that doesn't depend on per-request data. The handlers must not touch the database or session — they should be pure functions of the request URL.