Help us shape the future of Go! Take the 2026 Go Developer Survey

tug

package module
v0.50.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 54 Imported by: 0

README

tug

A web framework for Go apps whose frontend is Inertia.js: React pages rendered with props straight from Go handlers, with no API in between, and the whole app shipped as one binary.

The name: a tugboat is small, and moves ships many times its size.

Status: early. v0.50.0 is the latest release: the framework, its CLI, background jobs, on a schedule in any time zone too, pushed in the app's own transactions, and so many at once or a second across the instances, server-side rendering, tests of an app's pages, uploads kept on the app's disk or in S3, limits on routes and on guessing passwords, counted across the app's instances, the client's address behind its proxies, signed links, what tug and the app say in the request's language, a page's own words among it, which the document around it says it's in, the mail an account gets in the language it keeps, a date and a number written as that language writes them, a time in the reader's own zone, a page in a language written right to left laid out that way, lists in pages, the app's own commands, a cache and locks across the instances, mail with copies, files and a link to unsubscribe, downloads, streams and server-sent events, the app's own values encrypted, events broadcast to the pages open on every instance, what a user may do with a thing, headers that say what a browser may do with a page, and which of its powerful features none may use, a Content-Security-Policy that runs its own scripts and styles alone, the database's tables made and changed by files of SQL, each run once, the route that answered each request and how each job ran, for the app's metrics, each request under tug dev kept for Inertia's DevTools, the frontend's build sent gzipped by the app itself, forms typed by the structs their routes' handlers bind, flash data typed by its declared keys, jobs whose log lines say which request pushed them, a server error shown while debugging on a page of where it came from, in the app's source, the app's routes listed, each with the line that added it, a page's title and description given from Go, which a first visit's HTML has too, whole numbers past JavaScript's safe range sent as BigInts, the mail an app sends under tug dev kept in a mailbox it shows as a mail program would, HTTPS served by the app itself, with its certificates from Let's Encrypt, sessions kept on the server where they hold more than a cookie takes, and starters in React, Vue or Svelte, whose notes tell a coding agent how to change them, one with accounts, from registering to two-factor logins, passkeys, in place of a password too, a profile photo, API tokens, notifications in the app and by mail, and the browsers an account is logged in from, each to log out, in SQLite, Postgres or MySQL. The guide covers all of it, and docs/roadmap.md has how it was built.

go install github.com/cuonggt/tug/cmd/tug@latest
tug new blog                                        # a new app, in ./blog; tug new -auth blog for accounts
cd blog
tug dev                                             # http://localhost:8080, rebuilt and reloaded as it changes
type PostsIndexProps struct {
	Posts []Post                   `json:"posts"`
	Stats inertia.DeferProp[Stats] `json:"stats"` // fetched after the page shows
}

var PostsIndex = tug.Page[PostsIndexProps]("Posts/Index") // resources/js/pages/Posts/Index.tsx

func index(c *tug.Ctx) error {
	return PostsIndex.Render(c, PostsIndexProps{
		Posts: store.List(),
		Stats: inertia.Defer(store.Count),
	})
}

type PostInput struct {
	Title string `json:"title" validate:"required,max=80"`
	Body  string `json:"body" validate:"required"`
}

func create(c *tug.Ctx) error {
	var in PostInput
	if err := c.BindValid(&in); err != nil {
		return err // back to the form with its errors; a field being filled in is checked here too
	}
	post := store.Add(in)
	c.Flash("success", "Post created") // usePage().flash on the next page
	return c.RedirectRoute("posts.show", post.ID)
}

examples/inertia is the whole app: React pages, a deferred prop, forms that check each field as it's left, flash messages, posts that show in every browser open as they're made, the Vite dev server with hot reload, and the frontend embedded in the binary for production. Handlers that aren't pages look like this:

func main() {
	app := tug.New() // ADDR or PORT, and APP_DEBUG, from the environment
	app.Use(middleware.RequestID(), middleware.Logger(), middleware.Recover(), middleware.CSRF())

	app.Get("/posts/{id}", showPost).Name("posts.show")

	if err := app.Run(); err != nil { // graceful shutdown on SIGINT and SIGTERM
		log.Fatal(err)
	}
}

func showPost(c *tug.Ctx) error {
	var in struct {
		ID int64 `path:"id"`
	}
	if err := c.Bind(&in); err != nil {
		return err // /posts/abc is a 404
	}
	post, ok := posts[in.ID]
	if !ok {
		return tug.NewHTTPError(http.StatusNotFound, "post not found")
	}
	return c.JSON(http.StatusOK, post)
}

examples/api is a complete JSON API.

What's here

  • The CLI, tug: tug new makes an app, ready to run, with a .env and a fresh APP_KEY, and a frontend of React, or with -vue or -svelte, of Vue or Svelte. tug dev runs it with Vite: Go is rebuilt and restarted as it changes, and the browser reloaded. tug gen writes the TypeScript of each page's props and of the named routes, with a typed route(), of what each route's form sends, the struct its handler binds, with a form() of its path and method, and of the flash data's keys, so the frontend, its forms and flash among it, is checked against the Go. tug routes lists the app's routes, each with the line that added it, and its handler as that line has it. tug build makes one static binary with the frontend in it, which sends its scripts and styles gzipped, and the app comes with a Dockerfile for a distroless image, and an AGENTS.md, for a coding agent: how to add a page, a form, a table or a test as tug has it done, the rules the code doesn't show, and the commands that check a change.
  • Inertia pages (package inertia), to the whole v3 protocol: a first visit gets HTML with the page object, later visits get JSON. tug.Page[Props] ties a component to the props it takes, and props nest at any depth.
    • Loading: Lazy, Optional, Always and Defer props, worked out concurrently. Defer(...).Rescue() turns a failure into Inertia's rescue slot rather than a failed page.
    • Merging: Merge props, prepended or deep-merged, and matched on a key. Scroll pages a list for <InfiniteScroll>.
    • Pagination: tug.Paginate is the page of a list ?page asks for, from the app's own count and query, with where it sits and a pager's links, under the keys Laravel's paginators write; SimplePaginate without a count, and CursorPaginate by cursor. Each feeds Scroll too.
    • Once: Once props stay on the client until they expire.
    • Partial reloads reach nested props by path.
    • Redirects: a browser running an old build reloads. A 302 after a PUT, PATCH or DELETE becomes a 303, and a redirect to a #fragment keeps it.
    • Error pages: errors show as a page (Config.ErrorPage) with their own status.
    • Empty lists: nil slices go out as [], never null.
    • A page's head: c.Head gives a page its title, its description and the tags a link's preview is made from, from Go, each escaped, which the client keeps in the document's head, and a first visit's HTML has too, for search engines and previews, without SSR.
    • Big integers: inertia.BigInt goes to the page as a JavaScript BigInt, past the safe range of a number too, and is a bigint in TypeScript.
    • DevTools: under tug dev, each request is kept for Inertia's DevTools, the browser's panel: its route and the function that answered, where the page was rendered, each prop with its type and value, and the headers and bodies, with secrets redacted.
  • Server-side rendering (package ssr), when an app wants it, as tug new -ssr makes one: a first visit's page comes with its HTML, for search engines and pages that show before their scripts run, rendered by Inertia's own SSR in Node, which the app runs beside it, from the SSR build it embeds. Without Node, pages render in the browser, as they would anyway.
  • Forms: c.BindValid binds and checks a request by validate tags (package validate, go-playground/validator's rules, and the app's own, with validate.Rule) and checks of the handler's own. A form that doesn't validate goes back with its errors in the errors prop, under an error bag when the form names one; an API client gets a 422. A message names a field as a person reads it, "first name is required", by its label tag or its key made into words. Precognition, a form checking each field as it's left, is answered without running the rest of the handler.
  • Languages (package lang): what tug says to a person, a form's errors, a 429, an error page's status, and what the app says with c.T, in the request's language: the app's choice, or else the browser's Accept-Language, from a JSON file for each language, keyed by the English, as Laravel's lang/vi.json is, with plurals by each language's rules. A page says its own words in the language too, t('Say hello') and choice() over the same file, which tug gen writes the TypeScript of. tug lang vi writes the file, with every text the app and its pages say, and the root template says the language a page is in, <html lang="{{ .Locale }}">. Mail has no request to ask: a job says its words in the reader's own language, lang.Catalog.In's, which the auth starter keeps in a column of the account. A date and a number are written the language's way too: n() and d() in a page, the browser's own Intl, and Words.Number in Go, which a text's :count goes through. A time is in the app's zone, APP_TIMEZONE's, and in the reader's own once the page is in their browser. A language written right to left says so, <html dir="rtl">, as lang.Dir has it, and the starters' pages mirror themselves, laid out by what names a start and an end.
  • Encryption (package crypt): the app's own values, as a token for another service kept in a column, sealed with AES-256-GCM, under a key for their purpose alone, derived from APP_KEY, bound to their row if the app likes, and moved to a new key after a rotation, as tug's own are. tug key makes a key.
  • Sessions (package session): in an encrypted cookie, keyed by APP_KEY, with flash data. c.Flash reaches the next page shown, after a redirect or not, and only that one.
  • Files (package storage): uploads, from an Inertia form with its progress, kept in a directory the app serves, or in S3 or any service that speaks its API, Cloudflare R2 and MinIO among them, with S3's signatures on the standard library. validate checks an upload's size and what it is, from its first bytes rather than its name, and a page gets a link to it, public or signed until it expires. What's served can't run as the app: its type is its bytes', and anything but an image is a download.
  • Accounts: tug new -auth makes an app where people register and verify their email, or register with a passkey from a link mailed to them, with no password at all, log in, with a code from an authenticator app too once they turn that on, or with a passkey, reset a forgotten password, or get back in having lost their passkey, by email, change their profile and photo, password, set or removed, and appearance in settings, see the browsers they're logged in from and log one out, and make API tokens, for a script or another service to call the app's API with, hear of each change to their account that could hand it to someone else, a login from a new browser too, at once in the app, by its bell, and by mail, the old email too, with the users in SQLite, or with -postgres or -mysql, in Postgres or MySQL, and a frontend of Tailwind and shadcn's components, as Laravel's starter kits have, in React, Vue or Svelte. Its handlers are the app's own code, on package auth, which has the parts where a slip is a security hole: argon2id password hashes, logins that end when the password changes, each with an ID of its own, by which the app ends one from another browser, signed tokens for reset, verification and registration links, two-factor codes and recovery codes kept encrypted, passkeys, WebAuthn's checks on the standard library, asking for the password again, API tokens kept as their hashes, throttles on guessing, whose counts every instance of the app shares, and abilities, what a user may do with a thing, whose no is a 403 that says why.
  • Migrations (package migrate): the database's tables, made and changed by files of SQL, named for when they were made, which tug migrate new writes, each run once, in order, as the app starts, and by its binary's migrate command, which lists them and undoes the last. What ran is kept with a hash of its SQL, so a file changed after it ran is caught, and instances starting at once take turns. The auth starter's tables are its migrations, one for each, in SQLite, Postgres or MySQL.
  • Mail (package mail): through an SMTP server, or under tug dev kept in a mailbox the app shows at /_tug/mail, each mail as a mail program would show it, with copies, a Bcc no one sees, replies to another address, files, and a link to unsubscribe in one click, as Gmail and Yahoo ask of mail sent in bulk. mailtest.Outbox keeps what an app sends, for its tests, which read it in order, or by who it went to.
  • Background jobs (package queue): work a request starts and doesn't wait for, such as a mail, kept by a store such as a table in the app's database, so a failure or a restart doesn't lose it. A job that fails runs again after a wait that grows, and the workers run beside the server with app.Go, finishing what they have as the app stops. Jobs run on a schedule too, with Every or a cron expression, on UTC's clock or a time zone's, through its daylight saving as cron goes, once across all instances. A unique kind keeps one job waiting for each value, however often it's pushed, at the latest push's time if it likes, and runs them one at a time if it likes. A kind runs so many at once at most, or starts so many a second, counted on all the instances, and says, with OnFail, when a job has failed for good, and with Observe, how each run went, for the app's metrics. A job pushed in the app's own transaction is kept with what else it writes, or not at all. A job carries what the app lists from the context it was pushed from, and runs with it: the ID of the request that pushed it, which the queue's lines of it say, or a trace. The auth starter sends its mail this way, with its jobs in its database, where on Postgres or MySQL the claims of several instances skip each other's jobs rather than wait for them, and its binary lists the jobs that failed for good, with the request each came from, and runs them again: ./blog jobs.
  • Cache (package cache): what's slow to work out, kept for a while where every instance of the app finds it, such as a table in its database, as the auth starter has, with cache.Remember, which works a value out once however many ask for it at once, and locks that hold across the instances, for an import or a command that mustn't run twice at once.
  • Broadcasting (package broadcast): events on channels, as posts or users.42, to the pages that follow them on every instance of the app, through c.Events, carried by its database, as the auth starter has it: Postgres's NOTIFY, or a table in SQLite and MySQL, and in the transaction that makes the change, or not at all. A post made in one browser shows in the others, and the auth starter's page that asks to verify the email moves on once the link is followed on the phone.
  • Tests (package tugtest): Inertia's client, for Go's tests of an app's pages, which need no browser or frontend build. Its visits keep the cookies the app sets and follow its redirects, upload files as the browser does, and a page's props read into the struct its tug.Page declares: tugtest.Props(r, Dashboard).User. The starters' tests use it.
  • Vite (package vite): tags from the dev server while it runs, with the React refresh preamble, and from the build's manifest otherwise, with CSS and preloads, the scripts with the page's nonce. The built files are served, and cached for a year.
  • Routing on net/http's ServeMux: method routes, groups with their own middleware, and named routes with app.URL("posts.show", 42). Paths match exactly, so / is only the home page, and {name...} takes everything under a path. A trailing slash redirects to the route without it, and a wrong method is a 405 with Allow. Links that leave the app, as in mail, are whole, from APP_URL, never the request's Host, and app.SignedURL makes ones only the app could have, until they expire, for a route tug.Signed wraps. tug.Limit puts a limit on a route, by address or user: a 429, with Retry-After, past it. tug.RouteOf says which route answered a request, GET /posts/{id}, for the app's middleware to label its metrics by, and Logger logs it.
  • Handlers return errors. tug.NewHTTPError(404, "post not found") picks the status. Any other error is a 500 whose details stay in the log, or, with APP_DEBUG=true, show on a page of the error, a panic's stack with the app's source around it, the route that answered and the request, which Inertia's client shows in its modal. A panic is a 500 with its stack in the log. Errors are JSON for a client that asks for JSON.
  • Files, downloads and streams: c.File sends one of the app's files, with ranges, c.Download a file to save, its name however it's written, Vietnamese, quotes and all, c.StreamDownload a CSV made a row at a time, and c.Events server-sent events, for a page to follow work as it goes, ended as the app shuts down.
  • c.Bind fills a struct from path values, the query, and a JSON or form body, files included, by struct tags. A value that doesn't parse is a 400 that names the field, "age must be a whole number"; a bad path value is a 404.
  • Middleware is func(http.Handler) http.Handler: RequestID, Logger (through slog), Recover, CSRF, which is Go's http.CrossOriginProtection, so there are no tokens, CORS, for an API other sites' pages call, TrustProxies, which reads the client's address past the app's load balancers, from the end of X-Forwarded-For, for c.IP(), Headers, which says what a browser may do with a response, as be framed by another site, or use the camera, the microphone and the other powerful features no page asks for, and holds it to HTTPS, and CSP, a Content-Security-Policy that runs only the scripts that carry a nonce made for the response, which the root template gives Vite's tags and its own, and the styles too, with StyleNonce, and logs what browsers report it blocked.
  • app.Run listens on ADDR or PORT, and on SIGTERM stops taking connections and lets the requests in flight finish. With TLS_DOMAINS, on a machine of its own, it serves HTTPS itself, and HTTP/2, on :443, with a certificate for each domain from Let's Encrypt (package letsencrypt), asked for as it starts, kept, and renewed, and :80 sends each request to HTTPS; or with TLS_CERT and TLS_KEY, a certificate of the app's own, read again as it's renewed. Given one of the app's commands, as ./blog jobs, which app.Command adds, it runs that in place of serving, in the app as main made it.

tug needs Go 1.26. Its dependencies are go-playground/validator, for package validate, and golang.org/x/crypto, for argon2id in package auth and autocert in package letsencrypt, with golang.org/x/net, for the domain names autocert reads; the rest is the standard library.

Benchmarks

An Inertia page, a post and its comments, served by an app as tug new makes one, with its session, CSRF and Content-Security-Policy, in one process that uses every core:

Visits a second p99 First visits a second p99
tug 60,061 4.38 ms 56,146 5.09 ms
Start Memory, started Memory, under load Memory, after Size
tug 15 ms 6.4 MB 19.9 MB 19.3 MB 9.6 MB

Measured 2026-10-05 on Apple M1 Max, 10 cores, 32 GB, macOS 27.0.1.

docs/benchmarks.md has how these were measured, what tug adds to a request over ServeMux, Go's own router, the time it takes to render a page, and how to run them.

Development

go test -race ./...
go test -run '^$' -bench . -benchmem .    # tug next to ServeMux alone; bench/ has the rest
ADDR=127.0.0.1:8080 go run ./examples/api

The Inertia example, from examples/inertia:

npm install
npm run dev                               # the Vite dev server
ADDR=127.0.0.1:8080 go run .              # in another terminal
npm run build && npx playwright test      # end to end, in Chrome

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultErrorHandler

func DefaultErrorHandler(c *Ctx, err error)

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. So does an error that says its status with a StatusCode method, as package auth's no does, with its own Error as the message under 500, and the status's text from 500 up, as its Error may not be the client's. Anything else is a 500 that keeps its details in the log. 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. With Config.Debug on, a server error is shown with its details instead: as a page, to a client that takes HTML, as a browser and Inertia's client do, of the error and each it wraps, a panic's stack, the app's frames with the lines of source around them, the route that answered and where the app added it, and the request, its secrets [REDACTED]; with the same as JSON, to one that asks for JSON first; and as the text, the error and a panic's stack, to the rest.

func Generating added in v0.12.0

func Generating() bool

Generating reports whether tug gen started the app, to learn its pages and routes: Run writes their TypeScript then, and returns without serving. main runs up to Run either way, and what only serving needs, such as a database, it can leave alone while Generating says so, so that tug gen, and tug new and tug build, which run it, need nothing else running.

func RouteOf added in v0.29.0

func RouteOf(r *http.Request) string

RouteOf returns the route that answered r, as its String says it, "GET /posts/{id}": for the App's own middleware, around the router, to read once the handler has run, as the label of the request's metrics, which has as many values as the app has routes, where its path has as many as the app has posts. The route answers as it's matched, so its own middleware, and its group's, answer as it. A request no route answered, a 404, a 405, or a redirect to its path without its trailing slash, has "", as does one no router has seen.

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

func New(config ...Config) *App

New returns an App. Without a Config it reads one from the environment (ConfigFromEnv); a Config passed in is used as it is. It panics on a Config.URL that isn't the app's address, as a mistyped APP_URL should stop the app as it starts rather than make links that go nowhere.

func (*App) AbsoluteURL added in v0.14.0

func (a *App) AbsoluteURL(name string, params ...any) (string, error)

AbsoluteURL builds the whole link to the route named name, as URL builds its path, from Config.URL, the app's own address: AbsoluteURL("posts.show", 42) is "https://example.com/posts/42". It's for a link that leaves the app, as in mail; within the app, the path is enough. Without Config.URL it's an error.

func (*App) Command added in v0.17.0

func (a *App) Command(name, summary string, run func(ctx context.Context, args []string) error)

Command adds a command to the app's binary, for what it does besides serving: a fix to its data, an import, a user made an admin. Run, given the command's name as its first argument, runs it in place of serving, with the arguments after the name and a context canceled on SIGINT or SIGTERM, and returns its error: ./blog users:admin ann@example.com runs users:admin with ann@example.com. ./blog help lists the app's commands, with their summaries, and a name that isn't one is an error that says so.

app.Command("users:admin", "make a user an admin: users:admin <email>", func(ctx context.Context, args []string) error {
	if len(args) != 1 {
		return errors.New("users:admin takes the user's email")
	}
	return users.MakeAdmin(ctx, args[0])
})

A command runs in the app main made, with its routes, for the links it makes, its queue, for the jobs it pushes, and its database, but neither the server nor what Go runs: a job it pushes runs on the instances that serve. Its arguments are its own, to read with package flag or by hand. An app without commands serves whatever its arguments, and under tug gen, Run writes the types, whatever they are.

Command panics on a name that's taken, help, which lists them, or one that isn't a word of letters, digits and :-_, and once the app is serving.

func (*App) Go added in v0.3.0

func (a *App) Go(fn func(ctx context.Context) error)

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) Locale added in v0.15.0

func (a *App) Locale(r *http.Request) string

Locale returns the language of r: the one Config.Locale chooses, or else the best of Config.Lang's for the browser's Accept-Language, or else the app's default, as "vi" or "pt-BR". It's English without Config.Lang. For a handler, Ctx.Locale is the same; this is for code that has the request alone, as a props function shared with every page.

func (*App) Run

func (a *App) Run() error

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. With Config.TLS, it serves HTTPS there, and HTTP on TLS.HTTPAddr, and shuts both down together.

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. And for an app with commands, given one, Run runs it in place of serving: see Command.

func (*App) Serve

func (a *App) Serve(ctx context.Context, ln net.Listener) error

Serve serves on ln until ctx is done, then shuts down as Run does. It closes ln. A listener of TLS's serves HTTPS, and HTTP/2 when its tls.Config offers h2 in NextProtos, which http.Server adds for ServeTLS alone: Run's own HTTPS, with Config.TLS, offers it.

func (*App) ServeHTTP

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP answers a request. The first request fixes the routes and middleware: adding either after it panics.

func (*App) SignedURL added in v0.14.0

func (a *App) SignedURL(name string, expires time.Time, params ...any) (string, error)

SignedURL builds the whole link to the route named name, as AbsoluteURL does, signed with the app's key, Config.Keys, until expires: a link that only the app can have made, for a route that Signed wraps, such as an invitation's, mailed to whoever is invited.

The signature is over the link's path and its expiry, which the query carries as expires and signature. A link works until it expires, as often as it's followed: what must happen once, as an invitation accepted, is the app's to record.

func (*App) URL

func (a *App) URL(name string, params ...any) (string, error)

URL builds the path of the route named name, filling its wildcards with params in order: URL("posts.show", 42) is "/posts/42". Values are escaped, except for the slashes in a {name...} wildcard's value.

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
	// contains filtered or unexported fields
}

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".

func (*BindError) Error

func (e *BindError) Error() string

func (*BindError) Unwrap

func (e *BindError) Unwrap() error

type Certificates added in v0.40.0

type Certificates interface {
	GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
}

Certificates is a source of certificates for Run's HTTPS, as package letsencrypt's Manager, which gets them from Let's Encrypt: GetCertificate returns the one for a handshake, as tls.Config's does. It may have more, which Run looks for:

  • Hosts() []string, the names it has certificates for: Run's plain HTTP sends a request for one of them to HTTPS at its own host, and any other, as a scanner's by the machine's address, to Config.URL's, whose host has to be one of them, or Run doesn't start. Without it, a request goes to HTTPS at its own host, whatever it is.
  • HTTPHandler(fallback http.Handler) http.Handler, which Run's plain HTTP answers through, with the redirect as fallback, for what the source answers there, as Let's Encrypt's HTTP-01 challenges.
  • TLSConfig() *tls.Config, whose NextProtos the handshake offers beside h2 and http/1.1, as acme-tls/1, for Let's Encrypt's TLS-ALPN-01 challenges.
  • Run(ctx context.Context) error, which Run runs beside the server once both ports listen, as Go's work, as letsencrypt's asks for each domain's certificate as the app starts.

HTTPHandler and TLSConfig are autocert.Manager's own.

type Config

type Config struct {
	// Addr is where Run listens. Default ":8080", or with TLS, where it
	// serves HTTPS, ":443".
	Addr string

	// TLS has Run serve HTTPS itself, on Addr, with certificates from files
	// or from Let's Encrypt, by package letsencrypt, and plain HTTP beside
	// it, which sends each request to HTTPS. Its zero value serves plain
	// HTTP alone, for TLS ended in front of the app, at a proxy, a load
	// balancer or the platform.
	TLS TLS

	// Debug shows a server error with its details: a page of the error,
	// a panic's stack, with the lines of the app's source around its
	// frames, the route that answered, and the request, for a browser and
	// Inertia's client, and the same as JSON or text for the rest. It is
	// for development: in production they belong in the log only.
	Debug bool

	// Editor is the editor that Debug's page opens a frame's file in, at
	// its line, by a link: vscode, cursor, zed, goland or sublime, or a
	// link of the app's with {file} and {line} in it, as
	// "myeditor://open?file={file}&line={line}". ConfigFromEnv reads it
	// from APP_EDITOR.
	Editor string

	// 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

	// URL is the app's own address, a scheme and a host, as
	// https://example.com: what the links AbsoluteURL and SignedURL make
	// begin with, for links that leave the app, as in mail. It's never the
	// request's Host, which can name any site. New panics on one that isn't
	// an address.
	URL string

	// Keys sign the links SignedURL makes, and check them: the first signs,
	// and each of them checks, so the links made with a key being rotated
	// out still work until it's dropped. session.KeysFromEnv reads them from
	// APP_KEY and APP_PREVIOUS_KEYS.
	Keys [][]byte

	// Lang is the app's languages, which lang.Load reads: what tug says to
	// a person, a form's errors, a 429 or an error page's status, is said
	// in the request's language, as Ctx.Locale picks it, and so is what the
	// app says with Ctx.T. Without it, everything is in English.
	Lang *lang.Catalog

	// Locale is the language the app has chosen for a request, as a user's
	// choice, kept in their session or their account: one of Lang's, as
	// lang.Catalog.Find finds it, or "" to leave it to the browser's
	// Accept-Language. For a handler, the ErrorHandler and the pages it
	// renders, it runs inside the session's middleware, so it can read the
	// session; in the App's own middleware, there's none yet.
	Locale func(r *http.Request) string

	// Zone is the app's own time zone, an IANA name as "Asia/Ho_Chi_Minh",
	// which its pages write their times in: lang.Texts carries it to them
	// beside the language, for the date() tug gen writes. Empty is UTC. New
	// stops for a name that isn't a zone, which it reads from the host's own
	// files, as an image needs them too.
	Zone string

	// DevTools records each request for Inertia's DevTools, a panel of the
	// browser's, as an extension, which shows each visit's props, route,
	// headers and bodies, and where the page was rendered: an entry each,
	// kept in .tug/devtools, which it reads from two endpoints of the
	// app's, /_inertia/devtools/entries and the entries below it. It's for
	// development: tug dev turns it on, by TUG_DEV, which ConfigFromEnv
	// reads. Values under keys such as password and token, and headers
	// such as Cookie, are kept as [REDACTED].
	DevTools bool
}

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, APP_DEBUG, and APP_URL; TLS_CERT, TLS_KEY and HTTP_ADDR, for a certificate of the app's own; APP_EDITOR, for Debug's page; and DevTools, from TUG_DEV, which tug dev sets.

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) AbsoluteURL added in v0.14.0

func (c *Ctx) AbsoluteURL(name string, params ...any) (string, error)

AbsoluteURL builds the whole link to a named route, from the app's address, as App.AbsoluteURL does.

func (*Ctx) Bind

func (c *Ctx) Bind(dst any) error

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. The messages are in the request's language (see Ctx.Locale), and name a field by its label tag, or else by its key made into words.

func (*Ctx) BindValid

func (c *Ctx) BindValid(dst any, checks ...func(validate.Errors)) error

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". The messages are in the request's language (see Ctx.Locale); the checks' are theirs to say, with Ctx.T.

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) Blob

func (c *Ctx) Blob(code int, contentType string, b []byte) error

Blob writes b as the body, with the given content type.

func (*Ctx) Choice added in v0.15.0

func (c *Ctx) Choice(text string, n int, args ...any) string

Choice says text for a count in the request's language, as lang.Words.Choice does: c.Choice(":count post deleted|:count posts deleted", n).

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

func (c *Ctx) Context() context.Context

Context returns the request's context, which is canceled when the client goes away.

func (*Ctx) Download added in v0.20.0

func (c *Ctx) Download(name string, content io.Reader) error

Download sends content as a file to save, as name: an invoice made as a PDF, bytes.NewReader(pdf), or a file a disk's Open reads, under the name it was uploaded with. The browser saves it rather than show it, in Content-Disposition's words, which say the name however it's written: in Vietnamese, with quotes, or with a line break that can't end the header. What's before the name's last slash or backslash is dropped, as the browser saves a name, not a path.

Its type is its name's extension's, or else what its first bytes say. Content that seeks, as a file or a bytes.Reader does, goes through http.ServeContent, with ranges, and, when it has a Stat, as a file does, its time for If-Modified-Since. Other content goes as it's read, as Stream's body does. Download reads content to its end, and leaves it open: closing it is the caller's.

func (*Ctx) Events added in v0.20.0

func (c *Ctx) Events(fn func(ctx context.Context, send func(Event) error) error) error

Events sends a stream of server-sent events, which the browser's EventSource reads, as fn sends them: an import's progress, say, or a change a page should show, which it reloads its props for with Inertia's router.reload. The stream lasts as long as fn does: fn's context is done when the client goes away, and when the app shuts down, as Serve's Shutdown waits for the requests in flight, which a stream would hold on to. fn returns then; send fails once it's done.

app.Get("/imports/{id}/progress", func(c *tug.Ctx) error {
	return c.Events(func(ctx context.Context, send func(tug.Event) error) error {
		for p := range imports.progress(ctx, c.Param("id")) {
			if err := send(tug.Event{Name: "progress", Data: p}); err != nil {
				return err
			}
		}
		return nil
	})
})

An EventSource connects again when a stream ends, the app's shutdown's among them, after a few seconds, with the last event's ID in Last-Event-ID, for the app to go on from: a page that has what it waited for closes it. A quiet stream sends a comment every 20 seconds, which keeps the proxies in front of the app from closing it.

fn's error is Events', for the ErrorHandler, which logs it, as the response has started, unless the client went away or the app is shutting down, which is no failure. An event whose Name or ID has a line break, or whose Data JSON can't hold, is send's error, and isn't sent.

func (*Ctx) File added in v0.20.0

func (c *Ctx) File(path string) error

File sends the file at path, one of the app's own, such as a report it made, shown as its type says, HTML as a page: http.ServeContent sends it, with ranges, so a download that broke off goes on where it stopped, and If-Modified-Since. A file that isn't there, or a directory, is a 404 for the ErrorHandler.

path is the app's, never a request's, which could name any file: a file a request names is FileFS's, from a directory, as os.DirFS or an os.Root has it, which keeps it inside. A file someone uploaded goes by its disk's route, which sends it as what its bytes are, or by Download, to be saved.

func (*Ctx) FileFS added in v0.20.0

func (c *Ctx) FileFS(fsys fs.FS, name string) error

FileFS sends the file name in fsys, as File sends one: a file embedded in the app, or one of a directory a request names a file of, as FileFS(os.DirFS("docs"), c.Param("name")), where a name that would leave the directory, as "../app.db", isn't there.

func (*Ctx) Flash

func (c *Ctx) Flash(key string, value any)

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. A key the app declares with tug.Flash, for tug gen to type, panics for a value of another type than its own.

func (*Ctx) HTML

func (c *Ctx) HTML(code int, html string) error

HTML writes html as a page, exactly as given: anything from a user in it must already be escaped.

func (*Ctx) Head added in v0.37.0

func (c *Ctx) Head(head ...inertia.HeadElement)

Head gives the page this request renders the elements of its <head>: its title, its description, and the tags a link's preview is made from.

c.Head(inertia.Title(post.Title), inertia.Meta("description", post.Summary))

They go out as the page's head prop, which Inertia's client puts in the document's head with its serverHead option on, and in a first visit's HTML, through the root template's {{ .InertiaHead }}, for a search engine's crawler, or the app that makes a link's preview, which run no script. An element replaces the one before it of the same key, the handler's those of middleware, which gives them with inertia.WithHead.

func (*Ctx) IP added in v0.14.0

func (c *Ctx) IP() string

IP returns the address the request came from, without its port, as "203.0.113.9": a key for Limit that counts by address. Behind a proxy it's the proxy's, unless middleware.TrustProxies names the proxy, and reads the client's past it.

func (*Ctx) Inertia

func (c *Ctx) Inertia(component string, props any) error

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) JSON

func (c *Ctx) JSON(code int, v any) error

JSON writes v as JSON.

func (*Ctx) Locale added in v0.15.0

func (c *Ctx) Locale() string

Locale returns the request's language, as App.Locale picks it: the one the app chose for it with Config.Locale, or else the browser's, of the app's languages, or else the app's default.

func (*Ctx) Location

func (c *Ctx) Location(url string) error

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) NoContent

func (c *Ctx) NoContent(code int) error

NoContent writes a status and no body, such as 204.

func (*Ctx) Param

func (c *Ctx) Param(name string) string

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) Query

func (c *Ctx) Query(name string) string

Query returns the first value of the query parameter name.

func (*Ctx) Redirect

func (c *Ctx) Redirect(to string) error

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

func (c *Ctx) RedirectBack() error

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

func (c *Ctx) RedirectRoute(name string, params ...any) error

RedirectRoute redirects to the named route, filling its wildcards as App.URL does.

func (*Ctx) Request

func (c *Ctx) Request() *http.Request

Request returns the request.

func (*Ctx) Response

func (c *Ctx) Response() http.ResponseWriter

Response returns the response writer.

func (*Ctx) Session

func (c *Ctx) Session() *session.Session

Session returns the request's session, or nil when the app has no Config.Session.

func (*Ctx) SignedURL added in v0.14.0

func (c *Ctx) SignedURL(name string, expires time.Time, params ...any) (string, error)

SignedURL builds a signed link to a named route, as App.SignedURL does.

func (*Ctx) Stream added in v0.20.0

func (c *Ctx) Stream(contentType string, write func(w io.Writer) error) error

Stream sends a body as write makes it, of the type contentType, such as "text/csv", for a body too long to hold, made as it's read: every post as CSV, a row at a time, from rows as a query returns them. Each write goes to the client as it's made, flushed, so a client that reads as it comes, as a fetch of lines does, gets each one; a writer of many small writes wraps w in a bufio.Writer, as csv.Writer does itself.

write's error before its first write is Stream's, for the ErrorHandler, as any handler's is. Once the body has started, there's no error page to show: the error goes to the log, and the connection is cut, with http.ErrAbortHandler, so the client sees the body end before its time, where a browser would save half a file as the whole.

func (*Ctx) StreamDownload added in v0.20.0

func (c *Ctx) StreamDownload(name, contentType string, write func(w io.Writer) error) error

StreamDownload sends a body as write makes it, as Stream does, as a file to save, as name, which Download writes as it writes its own:

return c.StreamDownload("posts.csv", "text/csv; charset=utf-8", func(w io.Writer) error {
	out := csv.NewWriter(w)
	...
	out.Flush()
	return out.Error()
})

func (*Ctx) String

func (c *Ctx) String(code int, s string) error

String writes s as plain text.

func (*Ctx) T added in v0.15.0

func (c *Ctx) T(text string, args ...any) string

T says text in the request's language, as lang.Words.T does, from Config.Lang: c.T("Welcome back, :name", "name", user.Name), for a flash message. A text the language has no words for says itself, in English.

func (*Ctx) URL

func (c *Ctx) URL(name string, params ...any) (string, error)

URL builds the path of a named route, as App.URL does.

func (*Ctx) Validate

func (c *Ctx) Validate(v any, checks ...func(validate.Errors)) error

Validate checks v as BindValid checks what it binds, for values that don't come straight from the request. It answers Precognition requests the same way.

func (*Ctx) Written

func (c *Ctx) Written() bool

Written reports whether the response has started. After that its status and headers are on their way and can't change.

type CursorPaginated added in v0.16.0

type CursorPaginated[T any] struct {
	Data        []T     `json:"data"`
	Path        string  `json:"path"`
	PerPage     int     `json:"per_page"`
	NextCursor  *string `json:"next_cursor"`
	NextPageURL *string `json:"next_page_url"`
	// contains filtered or unexported fields
}

CursorPaginated is a page of a list by cursor, as CursorPaginate makes it, under the keys Laravel's cursor paginator writes: the cursor of the page after it, and its link, null on the last.

func CursorPaginate added in v0.16.0

func CursorPaginate[T, C any](c *Ctx, perPage int, fetch func(after *C, limit int) ([]T, error), cursor func(item T) C, opts ...PageOption) (CursorPaginated[T], error)

CursorPaginate is one page of a list by cursor, for a list whose items come and go as it's read, as a feed's: fetch gets the items after the one at the cursor, nil for the first page, limit of them, one more than a page, to know whether there's a next; cursor is where an item sits in the query's order, the app's own value, such as its time and its ID:

posts, err := tug.CursorPaginate(c, 20,
	func(after *PostCursor, limit int) ([]Post, error) { return store.PostsAfter(after, limit) },
	func(p Post) PostCursor { return PostCursor{At: p.CreatedAt, ID: p.ID} })

The link to the next page carries the last item's cursor, as JSON, in base64url, under ?cursor. It isn't signed, as Laravel's aren't: it's only where to start, and one made up starts somewhere else within what the query lets the page see. One that doesn't decode is the first page. Cursors go forward: a page before one runs the query the other way, which is the app's to write.

func (CursorPaginated[T]) Paging added in v0.16.0

func (p CursorPaginated[T]) Paging() inertia.Paging

Paging is where the page sits, for inertia.Scroll: the cursor it was asked for with, and the next page's.

type ErrorPageProps

type ErrorPageProps struct {
	Status  int    `json:"status"`
	Message string `json:"message"`
}

ErrorPageProps are the props of Config.ErrorPage.

type Event added in v0.20.0

type Event struct {
	// Name is the event's type, which a page listens for with
	// addEventListener, as "progress". Without one, it's a message, which
	// onmessage gets.
	Name string

	// ID is the event's, which the browser sends back when it connects
	// again, in Last-Event-ID, for the app to go on from after it. Optional.
	ID string

	// Data is what the event carries, as JSON, which the page reads with
	// JSON.parse: a value encoding/json can encode, or nil for none.
	Data any
}

Event is one of the server-sent events Ctx.Events sends.

type FlashOf added in v0.33.0

type FlashOf[T any] struct {
	// contains filtered or unexported fields
}

FlashOf is a key of the flash data, and the type of the value it carries. Flash declares one.

func Flash added in v0.33.0

func Flash[T any](key string) FlashOf[T]

Flash declares a key of the flash data a handler leaves for the next page shown, and the type of its value:

var Success = tug.Flash[string]("success")

func store(c *tug.Ctx) error {
	...
	Success.Set(c, "Post created")
	return c.RedirectRoute("posts.show", post.ID)
}

Declared this way, a key carries only values of its type, and tug gen writes the TypeScript of the keys declared, FlashData, as the type of a page's flash, usePage().flash, which a key no handler declares isn't in. Ctx.Flash of a declared key with a value of another type panics, as the frontend's type would say otherwise, and a key declared twice with different types panics.

func (FlashOf[T]) Key added in v0.33.0

func (f FlashOf[T]) Key() string

Key returns the key, as "success".

func (FlashOf[T]) Set added in v0.33.0

func (f FlashOf[T]) Set(c *Ctx, v T)

Set flashes v under the key, as Ctx.Flash does.

type HTTPError

type HTTPError struct {
	Code    int
	Message string
	Err     error
}

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, and in their language, with Ctx.T; without one, the status's text is shown, in the request's language. Err is the cause, for the log; only Config.Debug shows it, in a server error's response.

func NewHTTPError

func NewHTTPError(code int, message ...string) *HTTPError

NewHTTPError returns an HTTPError with the given status. Without a message it says the status's text, such as "Not Found".

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

type HandlerFunc

type HandlerFunc func(c *Ctx) error

HandlerFunc handles a request. A returned error is answered by the app's ErrorHandler: return an *HTTPError to choose the status.

func Limit added in v0.13.0

func Limit(l Limiter, key func(c *Ctx) string, h HandlerFunc) HandlerFunc

Limit wraps h so that each request counts a try with l, by the key that key makes of it: the address it came from, Ctx.IP, which behind a proxy takes middleware.TrustProxies, or the user's ID. A request over the limit doesn't reach h: it's a 429 for the app's ErrorHandler, with Retry-After in seconds, so an Inertia visit gets the error page, and an API's client JSON. An error from l is as though h returned it.

searches := &auth.Throttle{Name: "searches", Max: 30, Window: time.Minute}
app.Get("/search", tug.Limit(searches, (*tug.Ctx).IP, search))

func Signed added in v0.14.0

func Signed(h HandlerFunc) HandlerFunc

Signed wraps h so that only a link SignedURL made reaches it, before it expires. Any other request is a 403 for the app's ErrorHandler, which says whether the link has expired or isn't one the app made: one that was changed, or has anything in its query besides expires and signature, which Bind would give the handler.

app.Get("/invitations/{id}", tug.Signed(acceptInvitation)).Name("invitations.accept")

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 Limiter added in v0.13.0

type Limiter interface {
	Try(ctx context.Context, key string) (time.Duration, error)
}

A Limiter counts a try for a key, and says how long a key that has had its tries waits, or 0: Limit's one call, which *auth.Throttle has.

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware wraps a handler. It is net/http's own shape, so middleware written for net/http works as it is.

type PageLink struct {
	URL    *string `json:"url"`
	Label  string  `json:"label"`
	Active bool    `json:"active"` // it's the page shown
}

PageLink is a pager's link to a page, by its number, or its "...", which has no URL, for the pages it leaves out.

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

func Page[P any](component string) PageOf[P]

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.

func (PageOf[P]) Component

func (p PageOf[P]) Component() string

Component returns the page's component name, as "Posts/Index".

func (PageOf[P]) Render

func (p PageOf[P]) Render(c *Ctx, props P) error

Render renders the page with props, as Ctx.Inertia does.

type PageOption added in v0.16.0

type PageOption func(*pageOptions)

A PageOption changes how Paginate, SimplePaginate and CursorPaginate read the page asked for, and link the others.

func PageName added in v0.16.0

func PageName(name string) PageOption

PageName names the query parameter that picks the page, "page" by default, and "cursor" by cursor: another is for a second list on one page, as "comments_page", whose links leave the first list's page as it is.

type Paginated added in v0.16.0

type Paginated[T any] struct {
	CurrentPage  int        `json:"current_page"`
	Data         []T        `json:"data"`
	FirstPageURL string     `json:"first_page_url"`
	From         *int       `json:"from"` // the first item's place in the list, from 1, or null for a page with none
	LastPage     int        `json:"last_page"`
	LastPageURL  string     `json:"last_page_url"`
	Links        []PageLink `json:"links"` // the pages around this one, for a pager
	NextPageURL  *string    `json:"next_page_url"`
	Path         string     `json:"path"`
	PerPage      int        `json:"per_page"`
	PrevPageURL  *string    `json:"prev_page_url"`
	To           *int       `json:"to"` // the last item's place, or null
	Total        int        `json:"total"`
	// contains filtered or unexported fields
}

Paginated is a page of a list in numbered pages, as Paginate makes it: its items, and where it sits among the pages, under the keys Laravel's paginators write it with as JSON, so that a pager written for theirs reads it. The links are paths, with the query the list was shown with.

func Paginate added in v0.16.0

func Paginate[T any](c *Ctx, perPage int, count func() (int, error), fetch func(limit, offset int) ([]T, error), opts ...PageOption) (Paginated[T], error)

Paginate is one page of a list in numbered pages of perPage items: the one ?page asks for. count says how many items the list has, and fetch gets a page of them, by the limit and offset it's given, as SQL's LIMIT and OFFSET take them: the query is the app's, so tug has no SQL.

posts, err := tug.Paginate(c, 20, store.CountPosts, store.Posts)

A page that isn't a number, or is under 1, is the first, as Laravel has it, and a page past the last is empty, not a 404, as a list can shrink between two visits: count runs first, so fetch isn't asked for a page that has no items. The page size is the app's alone: a client that could pick it would pick how much the database reads.

func (Paginated[T]) Paging added in v0.16.0

func (p Paginated[T]) Paging() inertia.Paging

Paging is where the page sits, for inertia.Scroll, so that one query feeds numbered pages and <InfiniteScroll> alike:

inertia.Scroll(func() ([]Post, inertia.Paging, error) {
	posts, err := tug.Paginate(c, 20, store.CountPosts, store.Posts)
	return posts.Data, posts.Paging(), err
})

type PanicError

type PanicError struct {
	Value any
	Stack []byte
	// contains filtered or unexported fields
}

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 Props

type Props = inertia.Props

Props are a page's props, for a page without a struct of its own.

type Route

type Route struct {
	// contains filtered or unexported fields
}

Route is a route added to a Router. Name it to build its URL.

func (*Route) Name

func (rt *Route) Name(name string) *Route

Name names the route, for App.URL and Ctx.RedirectRoute. A name belongs to one route: naming a second route "posts.show" panics.

func (*Route) String

func (rt *Route) String() string

String describes the route by its method and path as it was added, "GET /posts/{id}", or "ANY /files/{path...}" for a route of any method.

func (*Route) Takes added in v0.32.0

func (rt *Route) Takes(input any) *Route

Takes declares the struct the route's handler binds, by a value of it, LoginInput{}, which a wrapper around the handler, as one that checks someone has logged in, keeps tug from seeing. tug gen writes it as what the route is sent, Inputs['login.store'] in routes.ts, which a form typed by it is checked against, as React's <Form<Inputs['login.store']>> is: its fields' keys, and its errors'.

The route needs its name first, as Inputs is by name: Takes panics on a route with none, and on a value that isn't a struct or a pointer to one. Bind fails, as a 500 whose error names both, a request of the route whose handler binds a struct of body fields other than input: Takes has drifted from the handler, and would type its forms against a struct the handler no longer reads.

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.

type SimplePaginated added in v0.16.0

type SimplePaginated[T any] struct {
	CurrentPage    int     `json:"current_page"`
	CurrentPageURL string  `json:"current_page_url"`
	Data           []T     `json:"data"`
	FirstPageURL   string  `json:"first_page_url"`
	From           *int    `json:"from"`
	NextPageURL    *string `json:"next_page_url"`
	Path           string  `json:"path"`
	PerPage        int     `json:"per_page"`
	PrevPageURL    *string `json:"prev_page_url"`
	To             *int    `json:"to"`
	// contains filtered or unexported fields
}

SimplePaginated is a page of a list in numbered pages without a count, as SimplePaginate makes it, under the keys Laravel's simple paginator writes: it knows whether there's a next page, and not how many there are.

func SimplePaginate added in v0.16.0

func SimplePaginate[T any](c *Ctx, perPage int, fetch func(limit, offset int) ([]T, error), opts ...PageOption) (SimplePaginated[T], error)

SimplePaginate is one page of a list in numbered pages, as Paginate is, for a list too long to count at each visit: fetch is asked for one item more than a page, to know whether there's a page after it.

func (SimplePaginated[T]) Paging added in v0.16.0

func (p SimplePaginated[T]) Paging() inertia.Paging

Paging is where the page sits, for inertia.Scroll.

type TLS added in v0.40.0

type TLS struct {
	// CertFile and KeyFile are a certificate, with its chain after it, and
	// its key, in PEM files, as Cloudflare's origin certificates or a
	// company's own CA's are. Run reads them as it starts, and again at a
	// handshake, once a minute at most, when either's time has changed, as
	// certbot's renewal replaces them; a pair that doesn't load leaves the
	// last in use, and the log says why. Both or neither, and not with
	// Certificates. ConfigFromEnv reads them from TLS_CERT and TLS_KEY.
	CertFile string
	KeyFile  string

	// Certificates is a source of certificates, in place of the files, as
	// package letsencrypt's Manager. A nil one is none, as the nil Manager
	// letsencrypt.FromEnv returns without TLS_DOMAINS, so what it returns
	// can go straight in.
	Certificates Certificates

	// HTTPAddr is where Run serves plain HTTP beside HTTPS: each request is
	// sent to its path over HTTPS, by a 308, but for what the source of
	// certificates answers there, as Let's Encrypt's challenges. Default
	// ":80". ConfigFromEnv reads it from HTTP_ADDR.
	HTTPAddr string
}

TLS has Run serve HTTPS itself, as on a machine of the app's own with no proxy in front to end TLS: HTTPS, and HTTP/2, on Config.Addr, ":443" unless it says another, and plain HTTP on HTTPAddr, which sends every request to HTTPS. The certificates are the files', CertFile and KeyFile, or a source's, Certificates, as package letsencrypt's, which gets them from Let's Encrypt. The zero value is off: Run serves plain HTTP on Config.Addr, for TLS ended in front of the app.

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:
passkeytest
Package passkeytest is a passkey authenticator in software, for tests of an app's passkeys.
Package passkeytest is a passkey authenticator in software, for tests of an app's passkeys.
throttletest
Package throttletest is for testing with auth.Throttle: TestStore checks that an auth.ThrottleStore keeps the promises a Throttle depends on.
Package throttletest is for testing with auth.Throttle: TestStore checks that an auth.ThrottleStore keeps the promises a Throttle depends on.
Package broadcast carries events between an app's instances, to the pages open on any of them: a post one instance saves, the pages on the others hear of, through a route of tug's Ctx.Events.
Package broadcast carries events between an app's instances, to the pages open on any of them: a post one instance saves, the pages on the others hear of, through a route of tug's Ctx.Events.
broadcasttest
Package broadcasttest is for testing with package broadcast: Memory is a Store in memory, and TestStore checks that a Store keeps the promises a Hub depends on.
Package broadcasttest is for testing with package broadcast: Memory is a Store in memory, and TestStore checks that a Store keeps the promises a Hub depends on.
Package cache keeps what's slow to work out, such as a dashboard's counts or what another service answered, for a while, in a store every instance of the app shares, such as a table in its database, and has locks for what mustn't run twice at once, on any of them.
Package cache keeps what's slow to work out, such as a dashboard's counts or what another service answered, for a while, in a store every instance of the app shares, such as a table in its database, and has locks for what mustn't run twice at once, on any of them.
cachetest
Package cachetest is for testing with package cache: TestStore checks that a cache.Store keeps the promises a cache.Cache depends on.
Package cachetest is for testing with package cache: TestStore checks that a cache.Store keeps the promises a cache.Cache depends on.
cmd
tug command
Command tug makes and runs tug apps:
Command tug makes and runs tug apps:
Package crypt encrypts the app's own values for keeping, such as a token for another service, which a user connected, kept in a column: a copy of the database, as a leaked backup, then has the token sealed, and nothing to open it with.
Package crypt encrypts the app's own values for keeping, such as a token for another service, which a user connected, kept in a column: a copy of the database, as a leaked backup, then has the token sealed, and nothing to open it with.
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, errors that come back as JSON to a client that asks for it, and a limit on how fast a client writes, by its address, behind the proxies TRUSTED_PROXIES names.
Command api is a small JSON API on tug: posts kept in memory, behind named routes in a group, with the usual middleware, errors that come back as JSON to a client that asks for it, and a limit on how fast a client writes, by its address, behind the proxies TRUSTED_PROXIES names.
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
devtools
Package devtools is the server's side of Inertia's DevTools, a panel of the browser's own, as an extension, that records each visit of an Inertia app: what the client did, paired with an entry of what the server says of each response, by the protocol at https://inertiajs.com/docs/v3/advanced/devtools-protocol, whose reference is inertia-laravel's.
Package devtools is the server's side of Inertia's DevTools, a panel of the browser's own, as an extension, that records each visit of an Inertia app: what the client did, paired with an entry of what the server says of each response, by the protocol at https://inertiajs.com/docs/v3/advanced/devtools-protocol, whose reference is inertia-laravel's.
filetype
Package filetype tells what a file is by its first bytes, as http.DetectContentType does, not by its name or by what a browser says it is, which are the user's to say.
Package filetype tells what a file is by its first bytes, as http.DetectContentType does, not by its name or by what a browser says it is, which are the user's to say.
frames
Package frames tells whose a frame of a goroutine's stack is: the app's own, tug's, the standard library's, or a dependency's.
Package frames tells whose a frame of a goroutine's stack is: the app's own, tug's, the standard library's, or a dependency's.
label
Package label names a field as a person reads it in a message: by its label tag, or else by the key a client sends it under, made into words.
Package label names a field as a person reads it in a message: by its label tag, or else by the key a client sends it under, made into words.
mailbox
Package mailbox keeps the mail an app sends under tug dev, rather than send it: package mail's Mailbox keeps each one in a directory, Dir, and the App shows them at Path, as a mail program would.
Package mailbox keeps the mail an app sends under tug dev, rather than send it: package mail's Mailbox keeps each one in a directory, Dir, and the App shows them at Path, as a mail program would.
nonce
Package nonce carries a response's Content-Security-Policy nonce in its request's context: package middleware makes it, and package inertia hands it to the root template, and neither imports the other.
Package nonce carries a response's Content-Security-Policy nonce in its request's context: package middleware makes it, and package inertia hands it to the root template, and neither imports the other.
route
Package route keeps the route that answered a request in its context: tug's App puts a place for it there as the request comes in, before any middleware, its router fills the place in as a route answers, and tug.RouteOf and package middleware's Logger read it once the handler has run, whatever copies of the request the middleware in between made.
Package route keeps the route that answered a request in its context: tug's App puts a place for it there as the request comes in, before any middleware, its router fills the place in as a route answers, and tug.RouteOf and package middleware's Logger read it once the handler has run, whatever copies of the request the middleware in between made.
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.
seal
Package seal encrypts values with AES-256-GCM, under a key for one use alone, derived by HKDF from each of the app's keys: the first seals, and each of them opens, so the app's key can be rotated.
Package seal encrypts values with AES-256-GCM, under a key for one use alone, derived by HKDF from each of the app's keys: the first seals, and each of them opens, so the app's key can be rotated.
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, with what each that takes one is sent.
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, with what each that takes one is sent.
ulid
Package ulid makes IDs that sort as they were made: ULIDs, 48 bits of the millisecond and 80 random ones, in 26 characters of Crockford's base32.
Package ulid makes IDs that sort as they were made: ULIDs, 48 bits of the millisecond and 80 random ones, in 26 characters of Crockford's base32.
Package lang says an app's texts in its languages: what tug says to a person, as a form's errors, and what the app does, as its flash messages and mail.
Package lang says an app's texts in its languages: what tug says to a person, as a form's errors, and what the app does, as its flash messages and mail.
Package letsencrypt gets an app's certificates from Let's Encrypt, for tug's Run to serve HTTPS itself, as on a machine of the app's own with no proxy in front to end TLS:
Package letsencrypt gets an app's certificates from Let's Encrypt, for tug's Run to serve HTTPS itself, as on a machine of the app's own with no proxy in front to end TLS:
Package mail sends email: through an SMTP server, or, while developing, into a mailbox the app shows under tug dev, or out to the terminal.
Package mail sends email: through an SMTP server, or, while developing, into a mailbox the app shows under tug dev, or out to the terminal.
mailtest
Package mailtest is for testing with package mail: Outbox is a Mailer that keeps what it's sent, for a test to read.
Package mailtest is for testing with package mail: Outbox is a Mailer that keeps what it's sent, for a test to read.
Package middleware is net/http middleware for tug apps.
Package middleware is net/http middleware for tug apps.
Package migrate runs an app's migrations: files of SQL in its migrations/ directory, each a change to its database's tables, run once, in the order of their names, which say when they were made:
Package migrate runs an app's migrations: files of SQL in its migrations/ directory, each a change to its database's tables, run once, in the order of their names, which say when they were made:
migratetest
Package migratetest has the promises a migrate.Store keeps, as tests, which each Store's own tests run on its database.
Package migratetest has the promises a migrate.Store keeps, as tests, which each Store's own tests run on its database.
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.
sessiontest
Package sessiontest is for testing with package session: TestStore checks that a session.Keeper keeps the promises a session.Store depends on.
Package sessiontest is for testing with package session: TestStore checks that a session.Keeper keeps the promises a session.Store depends on.
Package ssr renders Inertia pages on the server, for their first visits, so a page's HTML is in the response before its scripts run: for search engines, link previews, and pages that show sooner.
Package ssr renders Inertia pages on the server, for their first visits, so a page's HTML is in the response before its scripts run: for search engines, link previews, and pages that show sooner.
Package storage keeps files, such as what people upload: in a directory on the app's own disk, or in S3, or any service that speaks S3's API, such as Cloudflare R2 or MinIO.
Package storage keeps files, such as what people upload: in a directory on the app's own disk, or in S3, or any service that speaks S3's API, such as Cloudflare R2 or MinIO.
Package tugtest tests an app's pages as Inertia's client uses them, without a browser or a frontend build.
Package tugtest tests an app's pages as Inertia's client uses them, without a browser or a frontend build.
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL