Core Framework

Routing

Build routes on Go's ServeMux while preserving method handling and embedded public files.

By Guillermo Alvarez

Create the mux

Initialize the application context before creating routes:

ctx := appinit.Context(context.Background())
mux := lazyroutes.New(ctx)
appinit.Draw(ctx, mux)

lazyroutes.New creates an http.ServeMux and installs the embedded public handler at /.

Draw Application Routes

All application routes are registered through:

func Draw(ctx context.Context, mux *http.ServeMux)

Use lazyroutes.Bind when a route maps directly to one action:

mux.Handle(
    "GET /{$}",
    lazyroutes.Bind(
        ctx,
        home.New,
        (*home.HomeController).Index,
    ),
)

For REST-style controllers, register a resource:

lazyroutes.Resources(ctx, mux, posts.New)

Resources derives routes from the controller name. PostsController becomes:

GET    /posts             Index
GET    /posts/new         New
POST   /posts             Create
GET    /posts/{post_id}   Show
GET    /posts/{post_id}/edit Edit
PATCH  /posts/{post_id}   Update
PUT    /posts/{post_id}   Update
DELETE /posts/{post_id}   Delete

Only implemented actions are registered. An action still has the standard controller signature:

func (c *PostsController) Show(
    w http.ResponseWriter,
    r *http.Request,
) error

Path Values

Resource member routes derive their path value name from the singular resource name:

slug := r.PathValue("post_id")

Use GET /{$} for the root page only. Without {$}, / is a subtree pattern. The framework does not replace http.ServeMux; standard-library pattern precedence and conflict behavior still apply.

Custom Resource Routes

Add collection and member routes inside the resource configuration:

lazyroutes.Resources(ctx, mux, posts.New, func(r *lazyroutes.Resource[posts.PostsController]) {
    r.Get("search", (*posts.PostsController).Search)
    r.MemberGet("preview", (*posts.PostsController).Preview)
})

That registers:

GET /posts/search
GET /posts/{post_id}/preview

Override derived names when an application needs different paths or parameter names:

lazyroutes.Resources(ctx, mux, posts.New, func(r *lazyroutes.Resource[posts.PostsController]) {
    r.Path("articles")
    r.Param("slug")
})

Public fallback

The application context installs the public handler:

ctx = lazyroutes.WithPublic(
    ctx,
    http.FileServerFS(public),
)

The root fallback accepts GET and HEAD. Other methods receive 405 Method Not Allowed.

Do not install another root public handler in Draw; doing so would duplicate framework responsibility and can cause route conflicts.

Testing routes

Construct the complete handler without a network listener:

func application() http.Handler {
    ctx := appinit.Context(context.Background())
    mux := lazyroutes.New(ctx)
    appinit.Draw(ctx, mux)
    return mux
}

Use httptest.NewRequest and httptest.NewRecorder to verify status, headers, and response bodies.