Module API
Route Modules, a core component of the torque framework, are a type of http.Handler
that can handle requests of multiple different types. They take advantage of Golang's implicit interface implementations to make it easier to build your application. It enables torque to handle the wiring and plumbing of the application and leave you to focus on adding value for your users.
In torque
you can build Route Modules by implementing one or many of the interfaces in the Module API. The interfaces that your module implements define its behavior when handling incoming requests.
Loader
type Loader interface {
Load(req *http.Request) (any, error)
}
A Loader
is executed in response to an HTTP GET request made to the route where your module is registered. This is usually a navigation to a page in the browser, htmx hx-get
or curl request. The Loader
's responsibility is to fetch any data necessary to return in the response.
Here is an example loader for a login.html page:
func (rm *LoginRoute) Load(req *http.Request) (any, error) {
// formData might be present if the user reloads the page.
// we can pass it to our renderer to maintain their state
formData, err := torque.DecodeForm[model.LoginForm](req)
if err != nil {
return nil, err
}
// if the user is already authenticated via cookie we
// can just pass an error to the ErrorBoundary to handle
// the redirection
c, err := req.Cookie("authToken")
if err == nil && c.Expires.After(time.Now()) {
return nil, ErrAlreadyAuthenticated
}
// return some data to be passed to the Render function
return struct {
FormData *model.LoginForm `json:"-"`
}{
formData,
}, nil
}
Renderer
type Renderer interface {
Render(wr http.ResponseWriter, req *http.Request, loaderData any) error
}
Action
type Action interface {
Action(wr http.ResponseWriter, req *http.Request) error
}
An Action
enables your module to handle HTTP POST requests. This could be from a form submission in the browser, htmx’s hx-post
, curl or any client capable of sending HTTP requests.
The following example handles a form submission on a login.html page. torque
offers a series of utility functions to aid in parsing and validating incoming form data.
func (rm *LoginRoute) Action(wr http.ResponseWriter, req *http.Request) error {
// parse and validate the incoming form data
form, err := torque.DecodeAndValidateForm[model.LoginForm](req)
if err != nil {
return err
}
// call into our service layer to authenticate the user
authToken, err := rm.AuthService.Password(
req.Context(),
form.EmailAddress,
form.Password,
)
if err != nil {
return err
}
// set an http-only cookie with the auth token
http.SetCookie(wr, &http.Cookie{
Name: "authToken",
Value: *authToken,
Secure: true,
HttpOnly: true,
Expires: time.Now().Add(time.Hour * 36),
})
// finally, redirect to the /home page
http.Redirect(wr, req, "/home", http.StatusFound)
return nil
}
ErrorBoundary
type ErrorBoundary interface {
ErrorBoundary(wr http.ResponseWriter, req *http.Request, err error) http.HandlerFunc
}
An ErrorBoundary
handles all non-nil runtime error
values returned from other parts of the module such as the Action
, Loader
or Renderer
.
The boundary can return an alternate http.HandlerFunc
used to handle the failed request:
func (rm *LoginRoute) ErrorBoundary(wr http.ResponseWriter, req *http.Request, err error) http.HandlerFunc {
if errors.Is(err, ErrAlreadyAuthenticated) {
return torque.RedirectS(rm, "/home", http.StatusFound)
} else if errors.Is(err, auth.ErrInvalidPassword) {
return torque.RetryWithError(rm, err)
} else {
panic(err)
}
}
The torque framework offers a couple of useful error handlers:
Error Handlers | Description |
---|---|
Redirect | Returns an http.HandlerFunc that redirects the request to the given url and sets the statusCode to 302. |
RedirectS | Returns an http.HandlerFunc that redirects the request to the given url and sets the statusCode to the given code. |
RetryWithError | Attaches the given error value to the request context and re-executes the Loader → Renderer flow. |
RetryWithError
The RetryWithError
utility function allows one to re-execute the Loader
-> Renderer
flow with the given error
attached to the request context.
Note: Any errors returned by this handler automatically get sent to the PanicBoundary
Here is an updated example of what can be done in the Load
function with this additional context:
func (rm *LoginRoute) Load(req *http.Request) (any, error) {
// formData might be present if the user reloads the page.
// we can pass it to our renderer to maintain their state
formData, err := torque.DecodeForm[model.LoginForm](req)
if err != nil {
return nil, err
}
// check for any errors passed by `RetryWithError` and update
// the form's error message accordingly
err := torque.ErrorFromContext(req.Context())
if errors.Is(err, auth.ErrInvalidPassword) {
formData.ErrorMessage = "The username or password is invalid."
} else if err != nil {
panic(err) // unknown error, pass to the PanicBoundary
}
// if the user is already authenticated via cookie we
// can just pass an error to the ErrorBoundary to handle
// the redirection
c, err := req.Cookie("authToken")
if err == nil && c.Expires.After(time.Now()) {
return nil, ErrAlreadyAuthenticated
}
// return some data to be passed to the Render function
return struct {
FormData *model.LoginForm `json:"-"`
}{
formData,
}, nil
}
PanicBoundary
type PanicBoundary interface {
PanicBoundary(wr http.ResponseWriter, req *http.Request, err error) http.HandlerFunc
}
The PanicBoundary
catches all panics thrown during any Action
, Loader
, or Renderer
. It also catches any unhandled error
values from the ErrorBoundary
. Just like the ErrorBoundary
, this boundary is responsible for returning an alternate http.HandlerFunc
used to handle the failed request.
If no http.HandlerFunc
is returned from the PanicBoundary
, the error is safely logged and a stack trace is printed to stdout detailing the issue.
SubmoduleProvider
type SubmoduleProvider interface {
Submodules() []Route
}
A SubmoduleProvider
allows you to provide additional Route Modules to be registered as children of the current module. This is useful for creating a hierarchy of modules that can be registered to a single router.
Note that any modules returned from this function will be registered to a sub-router to the parent route. This means that any path prefix or Middleware
applied to the parent module will also be applied to the child modules.
For example, a module could declare its own file server with assets embedded via go:embed
//go:embed .build/static/*
var staticAssets embed.FS
func (rm *LoginRoute) Submodules() []torque.Route {
return []torque.Route{
// register file server at /login/s
torque.WithFileSystemServer("/s", staticAssets)
}
}
Global Type Assertion
It may be useful to create an anonymous type assertion for your struct type to ensure that it implements the interfaces you expect. This can be done by creating a function that returns your struct type and asserting it to the interface type.
var _ interface {
torque.Action
torque.Renderer
torque.Loader
} = (*MyRoute)(nil)