Documentation
¶
Overview ¶
Package tug is a web framework for Go apps whose frontend is Inertia.js.
This is the HTTP core the rest builds on. Routing is net/http's ServeMux, handlers return errors, and middleware has net/http's own shape, func(http.Handler) http.Handler, so anything written for net/http plugs in:
app := tug.New()
app.Use(middleware.RequestID(), middleware.Logger(), middleware.Recover())
app.Get("/posts/{id}", showPost).Name("posts.show")
if err := app.Run(); err != nil {
log.Fatal(err)
}
A handler gets a *Ctx for its request and returns an error. The app's ErrorHandler turns that error into the response, so a handler says what went wrong, as NewHTTPError(http.StatusNotFound), and never writes an error page itself.
The rest is in packages of their own, which don't import this one: inertia renders pages, session keeps sessions in a cookie, validate checks structs, vite puts a Vite build into pages, auth has the parts of accounts where a slip is a security hole, mail sends mail, and middleware has RequestID, Logger, Recover and CSRF. The guide is in the repository's docs directory: https://github.com/cuonggt/tug/tree/main/docs.
Index ¶
- func DefaultErrorHandler(c *Ctx, err error)
- type App
- type BindError
- type Config
- type Ctx
- func (c *Ctx) Bind(dst any) error
- func (c *Ctx) BindValid(dst any, checks ...func(validate.Errors)) error
- func (c *Ctx) Blob(code int, contentType string, b []byte) error
- func (c *Ctx) ClearHistory()
- func (c *Ctx) Context() context.Context
- func (c *Ctx) Flash(key string, value any)
- func (c *Ctx) HTML(code int, html string) error
- func (c *Ctx) Inertia(component string, props any) error
- func (c *Ctx) JSON(code int, v any) error
- func (c *Ctx) Location(url string) error
- func (c *Ctx) NoContent(code int) error
- func (c *Ctx) Param(name string) string
- func (c *Ctx) PreserveFragment()
- func (c *Ctx) Query(name string) string
- func (c *Ctx) Redirect(to string) error
- func (c *Ctx) RedirectBack() error
- func (c *Ctx) RedirectRoute(name string, params ...any) error
- func (c *Ctx) Request() *http.Request
- func (c *Ctx) Response() http.ResponseWriter
- func (c *Ctx) Session() *session.Session
- func (c *Ctx) String(code int, s string) error
- func (c *Ctx) URL(name string, params ...any) (string, error)
- func (c *Ctx) Validate(v any, checks ...func(validate.Errors)) error
- func (c *Ctx) Written() bool
- type ErrorPageProps
- type HTTPError
- type HandlerFunc
- type Middleware
- type PageOf
- type PanicError
- type Props
- type Route
- type Router
- func (r *Router) Any(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Delete(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Get(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Group(prefix string, mw ...Middleware) *Router
- func (r *Router) Handle(method, path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Options(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Patch(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Post(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Put(path string, h HandlerFunc, mw ...Middleware) *Route
- func (r *Router) Use(mw ...Middleware)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DefaultErrorHandler ¶
DefaultErrorHandler answers an error from a handler. validate.Errors go back to the form they came from, as Inertia expects, or to an API client as a 422 (see BindValid). An *HTTPError chooses the status and message. Anything else is a 500 that keeps its details in the log, unless Config.Debug is on. Server errors are logged through slog.Default(), with the stack for a panic.
The body is JSON, {"message": "..."}, when the request's Accept header asks for JSON first, and plain text otherwise.
Types ¶
type App ¶
type App struct {
*Router
// contains filtered or unexported fields
}
App is a web application: its routes, middleware and settings. It is an http.Handler, so an http.Server set up by hand can serve it too.
func New ¶
New returns an App. Without a Config it reads one from the environment (ConfigFromEnv); a Config passed in is used as it is.
func (*App) Go ¶ added in v0.3.0
Go runs fn beside the server, for work that isn't a request, such as a job queue's workers: Run and Serve start it as they start serving, with a context that's done as the app shuts down, and wait for it to return. An error it returns before then shuts the app down, and Serve returns it: an app whose jobs have stopped shouldn't carry on as though they hadn't. ServeHTTP, as tests call it, starts nothing, and nor does tug gen.
Go panics once the app is serving.
func (*App) Run ¶
Run serves on Config.Addr until SIGINT or SIGTERM, then shuts down gracefully: it stops taking connections, waits up to Config.ShutdownTimeout for the requests in flight to finish, and waits for what Go runs to return.
Run is also where tug gen learns about the app: started by it, with TUG_GEN set to a file, Run writes the TypeScript for the app's pages and named routes there, and returns without serving.
func (*App) Serve ¶
Serve serves on ln until ctx is done, then shuts down as Run does. It closes ln.
type BindError ¶
type BindError struct {
Field string // the key the value came under: a form or query key, JSON path or path wildcard
Reason string // what the value has to be, as "must be a whole number"
Err error // the parse error underneath
}
BindError is a request value that doesn't fit the field it was bound to, such as "abc" for an int. Its message is for the person who sent the value: "age must be a whole number".
type Config ¶
type Config struct {
// Addr is where Run listens. Default ":8080".
Addr string
// Debug puts error details and panic stacks into 500 responses. It is
// for development: in production they belong in the log only.
Debug bool
// ErrorHandler answers the errors handlers return, and the 404s and
// 405s of requests no route takes. Default DefaultErrorHandler.
ErrorHandler func(c *Ctx, err error)
// BodyLimit is the largest request body Bind reads, in bytes; a larger
// one is a 413. Default 32 MiB.
BodyLimit int64
// ShutdownTimeout is how long Run waits for requests in flight after
// SIGINT or SIGTERM. Default 10 seconds.
ShutdownTimeout time.Duration
// Inertia renders the app's pages, for Ctx.Inertia and Page.Render.
// With it set, every request also goes through its Middleware, inside
// the App's own middleware.
Inertia *inertia.Inertia
// ErrorPage is the Inertia page component that errors are shown with,
// such as "Error", with the props status and message: for a 404, a
// 403, or a 500 in production. API clients still get JSON, and with
// Debug on a 500 still shows its details. Without it, errors are plain
// text, which Inertia's client shows in a dialog.
ErrorPage string
// Session keeps each visitor's session, for Ctx.Session. It's what
// carries flash data and validation errors from a request to the page
// after it. With it set, every request goes through its Middleware,
// inside the App's own middleware.
Session *session.Store
}
Config is how an App behaves. Every field has a default, so the zero value works.
func ConfigFromEnv ¶
func ConfigFromEnv() Config
ConfigFromEnv reads the settings a deployment sets: ADDR, or PORT as platforms such as Cloud Run and Fly.io set it, and APP_DEBUG.
type Ctx ¶
type Ctx struct {
// contains filtered or unexported fields
}
Ctx is a request and its response, as a handler sees them. It belongs to its request: don't keep it after the handler returns.
func (*Ctx) Bind ¶
Bind fills the struct dst points to from the request. Each field's tags say where its value comes from:
type UpdatePost struct {
ID int64 `path:"id"` // the {id} in "/posts/{id}"
Draft bool `query:"draft"` // ?draft=1
Title string `json:"title"` // the body
}
The body is read by its Content-Type. JSON goes through encoding/json and its json tags. A form, urlencoded or multipart, fills a field by its form tag, or else its json tag's name, or else its name; a key that repeats, or ends in "[]", fills a slice. Files go to fields of type *multipart.FileHeader or []*multipart.FileHeader. An empty form value leaves its field alone, as an empty input means no value rather than zero.
Inertia's <Form> sends a form as JSON, with every value a string: "on" from a checkbox, "42" from a number input, "2026-09-25" from a date input, and "" from one left empty. So a JSON string where a bool, a number or a time.Time goes is read as a form's value is, and "" leaves its field alone; a JSON true or 42 binds as it is.
Query values are bound after the body, and path values last, so a body can't overwrite the ID in the URL.
A value that doesn't parse is a 400, or a 404 when it's a path value, since /posts/abc is a page that isn't there. The error wraps a *BindError naming the field. The fields after it are still bound, so that checking them, as BindValid does, finds what's really wrong with them rather than what's missing. A body over Config.BodyLimit is a 413, and a body Bind can't read is a 415.
func (*Ctx) BindValid ¶
BindValid binds the request into dst, as Bind does, then checks dst against its validate tags (see package validate) and against checks, which can add errors of their own, such as a title that's taken:
var in PostInput
err := c.BindValid(&in, func(errs validate.Errors) {
if posts.TitleTaken(in.Title) {
errs.Add("title", "title is taken")
}
})
if err != nil {
return err
}
When something's wrong it returns validate.Errors, which the ErrorHandler answers: a form goes back where it came from, with the errors, and an API client gets a 422. A value Bind can't parse is one of the errors: "age must be a whole number".
A Precognition request, sent by a form checking its fields as they're filled in, is answered here with a 204 or a 422, and BindValid returns an error, so that the handler stops before it changes anything: return the error as it is.
func (*Ctx) ClearHistory ¶
func (c *Ctx) ClearHistory()
ClearHistory has the next page shown tell the client to clear the history it has encrypted, as signing out should. See inertia.Config.EncryptHistory.
func (*Ctx) Context ¶
Context returns the request's context, which is canceled when the client goes away.
func (*Ctx) Flash ¶
Flash adds flash data for the next page shown, whether this request renders it or redirects to it: a message such as "Post created". The client reads it as usePage().flash, shows it once, and doesn't keep it in history. Surviving a redirect takes Config.Session.
func (*Ctx) HTML ¶
HTML writes html as a page, exactly as given: anything from a user in it must already be escaped.
func (*Ctx) Inertia ¶
Inertia renders an Inertia page component with props: an HTML page for a first visit, and the page object as JSON for a visit from Inertia's client. props is a struct or a map; see inertia.Inertia.Render. The app needs Config.Inertia.
func (*Ctx) Location ¶
Location sends the client to url with a full page load, which is how an Inertia app leaves for another site: see inertia.Location.
func (*Ctx) Param ¶
Param returns the value of the path wildcard name: Param("id") is "42" for /posts/42 on a "/posts/{id}" route.
func (*Ctx) PreserveFragment ¶
func (c *Ctx) PreserveFragment()
PreserveFragment has the next page shown tell the client to keep the #fragment of the visit that led to it, across the redirect: a comment sent from /posts/1#comments comes back to the comments.
func (*Ctx) Redirect ¶
Redirect sends the client to another URL: with 302 Found after a GET or HEAD, and 303 See Other after any other method. A 303 makes browsers, and Inertia, follow a PUT, PATCH or DELETE with a GET, where a 302 may repeat the method.
func (*Ctx) RedirectBack ¶ added in v0.2.0
RedirectBack redirects to the page the request came from, by its Referer, when that's a page of this app, and to "/" otherwise: a Referer can name any site. It's for a form that more than one page has, which goes back to whichever it was sent from.
func (*Ctx) RedirectRoute ¶
RedirectRoute redirects to the named route, filling its wildcards as App.URL does.
func (*Ctx) Response ¶
func (c *Ctx) Response() http.ResponseWriter
Response returns the response writer.
func (*Ctx) Session ¶
Session returns the request's session, or nil when the app has no Config.Session.
type ErrorPageProps ¶
ErrorPageProps are the props of Config.ErrorPage.
type HTTPError ¶
HTTPError is an error with a status. Return one from a handler to choose the response: NewHTTPError(http.StatusNotFound) is a 404.
Message is shown to the client, so it says what went wrong in their terms. Err is the cause, for the log; only Config.Debug shows it, in a server error's response.
func NewHTTPError ¶
NewHTTPError returns an HTTPError with the given status. Without a message it says the status's text, such as "Not Found".
type HandlerFunc ¶
HandlerFunc handles a request. A returned error is answered by the app's ErrorHandler: return an *HTTPError to choose the status.
func WrapHandler ¶
func WrapHandler(h http.Handler) HandlerFunc
WrapHandler turns an http.Handler into a HandlerFunc, to put something written for net/http, such as http.FileServer, on a route.
type Middleware ¶
Middleware wraps a handler. It is net/http's own shape, so middleware written for net/http works as it is.
type PageOf ¶
type PageOf[P any] struct { // contains filtered or unexported fields }
PageOf is an Inertia page component, and the props it takes. Page declares one.
func Page ¶
Page declares an Inertia page component, and the props it takes:
type PostsIndexProps struct {
Posts []Post `json:"posts"`
Stats inertia.DeferProp[Stats] `json:"stats"`
}
var PostsIndex = tug.Page[PostsIndexProps]("Posts/Index")
func index(c *tug.Ctx) error {
return PostsIndex.Render(c, PostsIndexProps{Posts: posts, Stats: inertia.Defer(countWords)})
}
Declared this way, a page can only be rendered with the props it takes, and tug gen writes their TypeScript, for the page component to take them too. A component declared twice with different props panics.
type PanicError ¶
PanicError is a handler's panic, recovered by the app and passed to its ErrorHandler like any other error, with the stack for the log.
func (*PanicError) Error ¶
func (e *PanicError) Error() string
func (*PanicError) Unwrap ¶
func (e *PanicError) Unwrap() error
Unwrap returns the panic's value when that was an error.
type Route ¶
type Route struct {
// contains filtered or unexported fields
}
Route is a route added to a Router. Name it to build its URL.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router adds routes. The App is the root Router, and Group makes one for a path prefix, with middleware of its own.
A path matches exactly: "/posts" is /posts and nothing under it, and "/" is only the home page. End a path with a {name...} wildcard to take everything under it. Beyond that, paths are net/http ServeMux patterns, with its wildcards and its rule that the more specific pattern wins.
func (*Router) Any ¶
func (r *Router) Any(path string, h HandlerFunc, mw ...Middleware) *Route
Any adds a route for requests to path under any method.
func (*Router) Delete ¶
func (r *Router) Delete(path string, h HandlerFunc, mw ...Middleware) *Route
Delete adds a route for DELETE requests to path.
func (*Router) Get ¶
func (r *Router) Get(path string, h HandlerFunc, mw ...Middleware) *Route
Get adds a route for GET requests to path, which also takes HEAD.
func (*Router) Group ¶
func (r *Router) Group(prefix string, mw ...Middleware) *Router
Group returns a Router for routes under prefix, such as "/admin", that run mw after the middleware of r. An empty prefix groups routes for the middleware alone.
func (*Router) Handle ¶
func (r *Router) Handle(method, path string, h HandlerFunc, mw ...Middleware) *Route
Handle adds a route for method requests to path; an empty method takes them all. mw wraps this route alone, inside its groups' middleware.
Handle panics when path is not a valid pattern, or takes the same requests as a route already added, as ServeMux does: both are mistakes to fix before the app can start.
func (*Router) Options ¶
func (r *Router) Options(path string, h HandlerFunc, mw ...Middleware) *Route
Options adds a route for OPTIONS requests to path.
func (*Router) Patch ¶
func (r *Router) Patch(path string, h HandlerFunc, mw ...Middleware) *Route
Patch adds a route for PATCH requests to path.
func (*Router) Post ¶
func (r *Router) Post(path string, h HandlerFunc, mw ...Middleware) *Route
Post adds a route for POST requests to path.
func (*Router) Put ¶
func (r *Router) Put(path string, h HandlerFunc, mw ...Middleware) *Route
Put adds a route for PUT requests to path.
func (*Router) Use ¶
func (r *Router) Use(mw ...Middleware)
Use adds middleware, which runs in the order added. On the App it wraps every request, including those no route takes, which suits logging and recovery. On a group it wraps the group's routes, inside the middleware of the groups around it, including routes added before the Use.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package auth has the parts of logging users in where a slip is a security hole, for an app that keeps its users where it likes:
|
Package auth has the parts of logging users in where a slip is a security hole, for an app that keeps its users where it likes: |
|
cmd
|
|
|
tug
command
Command tug makes and runs tug apps:
|
Command tug makes and runs tug apps: |
|
examples
|
|
|
api
command
Command api is a small JSON API on tug: posts kept in memory, behind named routes in a group, with the usual middleware, and errors that come back as JSON to a client that asks for it.
|
Command api is a small JSON API on tug: posts kept in memory, behind named routes in a group, with the usual middleware, and errors that come back as JSON to a client that asks for it. |
|
inertia
command
Command inertia is tug's example of an Inertia app: posts, shown by React pages that get their props from Go handlers, and written with forms that check themselves as they're filled in.
|
Command inertia is tug's example of an Inertia app: posts, shown by React pages that get their props from Go handlers, and written with forms that check themselves as they're filled in. |
|
Package inertia is the server side of the Inertia.js v3 protocol, for net/http.
|
Package inertia is the server side of the Inertia.js v3 protocol, for net/http. |
|
internal
|
|
|
rw
Package rw wraps an http.ResponseWriter to remember what went through it: the status, and so whether the response has started, which an error handler has to know before it writes an error page, and the size, for the log.
|
Package rw wraps an http.ResponseWriter to remember what went through it: the status, and so whether the response has started, which an error handler has to know before it writes an error page, and the size, for the log. |
|
typegen
Package typegen writes the TypeScript that mirrors a tug app's Go: the props each page takes, the props every page shares, and the app's named routes.
|
Package typegen writes the TypeScript that mirrors a tug app's Go: the props each page takes, the props every page shares, and the app's named routes. |
|
Package mail sends email: through an SMTP server, or, while developing, out to the terminal, where a link in it can be clicked.
|
Package mail sends email: through an SMTP server, or, while developing, out to the terminal, where a link in it can be clicked. |
|
Package middleware is net/http middleware for tug apps.
|
Package middleware is net/http middleware for tug apps. |
|
Package queue runs jobs in the background: work a request starts and doesn't wait for, such as sending a mail.
|
Package queue runs jobs in the background: work a request starts and doesn't wait for, such as sending a mail. |
|
queuetest
Package queuetest is for testing with package queue: Memory is a Store for tests, and TestStore checks that a Store keeps the promises the queue depends on.
|
Package queuetest is for testing with package queue: Memory is a Store for tests, and TestStore checks that a Store keeps the promises the queue depends on. |
|
Package session keeps a visitor's session in an encrypted cookie, with flash data: values one request leaves for the next, such as a message after a redirect or the validation errors a form is sent back with.
|
Package session keeps a visitor's session in an encrypted cookie, with flash data: values one request leaves for the next, such as a message after a redirect or the validation errors a form is sent back with. |
|
Package validate checks a struct against the rules in its validate tags, and says what's wrong in words for the person who filled in the form:
|
Package validate checks a struct against the rules in its validate tags, and says what's wrong in words for the person who filled in the form: |
|
Package vite puts a frontend built with Vite into pages that Go renders: the tags that load it, from the dev server while that runs and from the build's manifest otherwise, and a handler that serves the built files.
|
Package vite puts a frontend built with Vite into pages that Go renders: the tags that load it, from the dev server while that runs and from the build's manifest otherwise, and a handler that serves the built files. |