Muxt
Early WIP (not yet tested in prod)
Sometimes as a developer it is nice to stay in an HTML headspace. This Go code generator helps you do that.
It also provides a nice test seam between your http and endpoint handlers.
Muxt generates Go code. It does not require you to add any dependencies outside the Go standard library.
- It allows you to register HTTP routes from HTML templates
- It generates handler functions and registers them on an
*http.ServeMux
- It generates code in handler functions to parse path parameters and form fields
- It generates a receiver interface to represent the boundary between your app code and HTTP/HTML
- Use this to mock out your server and test the view layer of your application
Installation
You can install it using the Go toolchain.
cd # Change outside of your module (so you don't add muxt to your dependency chain)
go install github.com/crhntr/muxt@latest
cd -
You do not need to add this tool to your module (unless you want to use the tools pattern).
Usage
Generate Command
This command is how you use muxt.
You can call it either by invoking it from your terminal or by adding a generate comment to a Go source file.
Flags
Usage of generate:
-output-file string
The generated file name containing the routes function and receiver interface. (default "template_routes.go")
-receiver-interface string
The interface name in the generated output-file listing the methods used by the handler routes in routes-func. (default "RoutesReceiver")
-receiver-type string
The type name for a named type to use for looking up method signatures. If not set, all methods added to the receiver interface will have inferred signatures with argument types based on the argument identifier names. The inferred method signatures always return a single result of type any.
-receiver-type-package string
The package path to use when looking for receiver-type. If not set, the package in the current directory is used.
-routes-func string
The function name for the package registering handler functions on an *"net/http".ServeMux.
This function also receives an argument with a type matching the name given by receiver-interface. (default "routes")
-templates-variable string
the name of the global variable with type *"html/template".Template in the working directory package. (default "templates")
Shell Example
If you invoke it from the shell, it expects to find a Go source package in the current directory where it can find a templates variable.
muxt generate
If you do the generate comment, make sure you need to write the comment in the same package as your (globally scoped) templates
variable.
package main
import (
"embed"
"html/template"
)
//go:generate muxt generate
var (
//go:embed *.gohtml
templatesSource embed.FS
templates = template.Must(template.ParseFS(templatesSource, "*.gohtml"))
)
Making Template Source Files Discoverable
Muxt needs your template source files to be embedded in the package in the current directory for it to discover and parse them (see the "Go Generate Comment Example" above).
You need to add a globally scoped variable with type embed.FS
(like templatesSource
in the example).
It should be passed into a call either the function "html/template".ParseFS
or method "html/template".Template.ParseFS
.
Before it does so, it can call any of the following functions or methods in the right hand side of the templates
variable declaration.
Muxt will call any of the functions:
or methods:
Muxt iterates over the resulting parsed templates to discover templates matching the template name pattern documented in the "Naming Templates" section below.
Naming Templates
muxt generate
will read your embedded HTML templates and generate/register an http.HandlerFunc
for each template with a name that matches an expected patten.
If the template name does not match the pattern, it is ignored by muxt.
Since Go 1.22, the standard library route multiplexer can parse path parameters.
It has expects strings like this
[METHOD ][HOST]/[PATH]
Muxt extends this a little bit.
[METHOD ][HOST]/[PATH ][HTTP_STATUS ][CALL]
A template name that muxt understands looks like this:
{{define "GET /greet/{language} 200 Greeting(ctx, language)" }}
<h1>{{.Hello}}</h1>
{{end}}
In this template name
- Passed through to
http.ServeMux
- we define the HTTP Method
GET
,
- the path prefix
/greet/
- the path parameter called
language
(available in the call scope as language
)
- Used by muxt to generate a
http.HandlerFunc
- the status code to use when muxt calls WriteHeader is
200
aka http.StatusOK
- the method name on the configured receiver to call is
Greeting
- the parameters to pass to
Greeting
are ctx
and language
Here is an excerpt from the standard libary documentation.
Patterns can match the method, host and path of a request. Some examples:
- "/index.html" matches the path "/index.html" for any host and method.
- "GET /static/" matches a GET request whose path begins with "/static/".
- "example.com/" matches any request to the host "example.com".
- "example.com/{$}" matches requests with host "example.com" and path "/".
- "/b/{bucket}/o/{objectname...}" matches paths whose first segment is "b" and whose third segment is "o". The name "bucket" denotes the second segment and "objectname" denotes the remainder of the path.
The Method Call Scope
There are three parameters you can pass to a method that always generate the same code
ctx
-> http.Request.Context
request
-> *http.Request
response
-> http.ResponseWriter
(if you use this, muxt won't generate code to call WriteHeader, you have to do this)
Using these three, the generated code will look something like this.
Given {{define "GET / F(ctx, response, request)"}}Hello{{end}}
,
You will get a handler generated like this:
package main
import (
"context"
"net/http"
)
type RoutesReceiver interface {
F(ctx context.Context, response http.ResponseWriter, request *http.Request) any
}
func routes(mux *http.ServeMux, receiver RoutesReceiver) {
mux.HandleFunc("GET /", func(response http.ResponseWriter, request *http.Request) {
ctx := request.Context()
data := receiver.F(ctx, response, request)
execute(response, request, false, "GET / F(ctx, response, request)", http.StatusOK, data)
})
}
func execute(http.ResponseWriter, *http.Request, bool, string, int, any) {}
You can also map path values from the path pattern to identifiers and pass them to your handler.
Given {{define "GET /articles/{id} ReadArticle(ctx, id)"}}{{end}}
,
You will get a handler generated like this:
package main
import (
"context"
"net/http"
)
type RoutesReceiver interface {
ReadArticle(ctx context.Context, id string) any
}
func routes(mux *http.ServeMux, receiver RoutesReceiver) {
mux.HandleFunc("GET /articles/{id}", func(response http.ResponseWriter, request *http.Request) {
ctx := request.Context()
id := request.PathValue("id")
data := receiver.ReadArticle(ctx, id)
execute(response, request, true, "GET /articles/{id} ReadArticle(ctx, id)", http.StatusOK, data)
})
}
func execute(http.ResponseWriter, *http.Request, bool, string, int, any) {}
TODO add more documentation on form and typed arguments
Examples
The example directory has a worked example.
For a more complete example, see: https://github.com/crhntr/muxt-template-module-htmx