Go + Inertia.js v3
React pages straight from Go handlers, with no API in between.
tug is a web framework for Go apps whose frontend is Inertia.js. Handlers render React, Vue or Svelte pages with typed props, and the whole app ships as one binary.
$ go install github.com/cuonggt/tug/cmd/tug@latest
$ tug new blog # -auth for accounts
$ cd blog
$ tug dev
tug │ serving http://localhost:8080, built in 268ms
^C
$ tug build
tug build: blog, 9.6 MB, frontend included
How it fits together
A Go struct is the page's props, and its types.
A handler renders a page with a struct, which the page's component gets as its props. tug gen writes the struct's TypeScript, so the component is checked against the Go, and tug dev writes it again as the Go changes.
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: store.List(),
Stats: inertia.Defer(store.Count), // after the page shows
})
}// Code generated by tug gen from the Go types. DO NOT EDIT.
export interface PostsIndexProps {
posts: Post[]
stats?: Stats
}import { Deferred, Link } from '@inertiajs/react'
import type { PageProps } from '../../tug/pages'
import { route } from '../../tug/routes'
type Props = PageProps<'Posts/Index'>
export default function Index({ posts, stats }: Props) {
return (
<>
<Deferred data="stats" fallback={<p>Counting…</p>}>
{stats && <p>{stats.posts} posts</p>}
</Deferred>
{posts.map((post) => (
<Link
key={post.id}
href={route('posts.show', { id: post.id })}
>
{post.title}
</Link>
))}
</>
)
}What's in it
The parts an app needs, already working with Inertia.
Sessions, flash messages, validation errors and CSRF work with Inertia without setup, on the standard library underneath.
Inertia v3, whole
Deferred, merged, once and optional props, partial reloads by path, infinite scroll, and a page's title and description from Go.
Types from Go
tug genwrites the TypeScript of each page's props, the named routes, what each form sends and the flash data's keys.Forms that check themselves
c.BindValidbinds a request and checks it by its struct tags. A form that doesn't validate goes back with its errors, and each field is checked as it's left.Accounts, from one flag
tug new -auth: registering, verified email, two-factor codes, passkeys, API tokens and notifications, in SQLite, Postgres or MySQL.Background jobs
Jobs that run again when they fail, on a schedule in any time zone, once however often they're pushed, and in the app's own transaction.
One binary
tug buildmakes one static binary with the frontend in it, sent gzipped by the app itself, and the app comes with a Dockerfile.Rendered on the server, if you like
tug new -ssrrenders a first visit's page with Inertia's own SSR, in Node beside the app. Without Node, pages render in the browser.Tests without a browser
Package
tugtestis Inertia's client for Go's tests: visits, forms, uploads and logins, with a page's props read into its Go struct.Events on every instance
Package
broadcastcarries events on channels through the app's database, to the pages open on every instance, which reload what changed.
Why Go
One small binary, on every core, that starts in milliseconds.
Go makes a web app one static binary, which uses every core in one process, starts in milliseconds, and runs in tens of MB of memory. tug keeps all of it for an Inertia app: one static binary, its frontend's build inside it, served by Go's own HTTP server.
Performance
Tens of thousands of requests a second on a laptop, in one process that uses every core, in tens of MB of RAM.
Startup
In milliseconds, which keeps the cost down on a platform that scales to zero.
Deploying
One file to copy, with no runtime or dependencies to install, built for Linux or ARM from a Mac.
Development
A small language that compiles fast, with an HTTP server in its standard library that's good enough for production.
How fast
60,061 visits a second, in 19.9 MB of memory.
One 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.
-
60,061 visits a second
Inertia's client asking for the page, answered as JSON, 99 in 100 within 4.38 ms.
-
56,146 first visits a second
A browser asking for the page, answered with its HTML, 99 in 100 within 5.09 ms.
-
19.9 MB of memory under load
6.4 MB once it has started, idle.
-
15 ms to start
From starting the binary to its first answer to the page.
-
9.6 MB to deploy
One static binary, with nothing to install beside it.
-
75 ns added to a request
What tug's App adds to ServeMux, Go's own router, in one process.
Measured on Apple M1 Max, 10 cores, macOS 27.0.1. Every number, and how they were measured
The guide
From a new app to a deployed one.
18 parts, in the order an app needs them. Each package's own documentation, on pkg.go.dev, has the rest of the detail.
- Getting started
install tug, make an app, run it, and add a page and a form.
- Routing and handlers
the App, routes, whole and signed links, groups, middleware, security headers among them, the route that answered, the client's address behind a proxy,
Ctx, files, downloads, streams and events, binding requests, and errors. - Pages
Inertia pages and their props, the root template and its nonce, the props worked out later, shared props, redirects, downloads and events on a page, error pages, Vite, and Inertia's DevTools.
- Server-side rendering
pages rendered on the server for a first visit, by Node beside the app, and package
ssr. - Forms and sessions
validation, rules of the app's own, forms that check each field as it's left, flash messages, sessions, in their cookies or kept on the server, and CSRF.
- Languages
what tug and the app say, in the request's language, from a file for each, package
lang, andtug lang. - Files
uploads, checked by their size and what they are, kept on the app's disk or in S3 with package
storage, and links to them, public or signed. - Accounts
the auth starter, in SQLite, Postgres or MySQL, its API tokens and notifications, and packages
authandmail, with copies, files and a link to unsubscribe in one click. - Migrations
the database's tables, made and changed by files of SQL, each run once, in order, as the app starts, and by its
migratecommand, withtug migrate newfor the next, and packagemigrate. - Authorization
what a user may do with a thing, package
auth's abilities, a no as a 403 that says why, and what a page's user may do in its props. - Encryption
the app's key, what tug encrypts and signs with it, package
crypt, for the app's own values, and rotating the key. - Background jobs
package
queue, for work that outlasts the request, and runs again when it fails, runs on a schedule, in a time zone too, waits once however often it's asked for, or so many at once or a second, on every instance, and says when it has failed for good, and how each run went. - Cache
package
cache, for what's slow to work out, kept where every instance of the app finds it, and locks for what mustn't run twice at once. - Broadcasting
package
broadcast, events on channels, carried by the app's database to the pages open on every instance, which reload what changed. - TypeScript
the types
tug genwrites from the Go. - Testing
package
tugtest, Inertia's client for Go's tests of an app's pages, forms, uploads and logins,mailtest, for its mail, and its events. - Deployment
one binary, the Dockerfile, the environment, HTTPS served by the app itself, with certificates from Let's Encrypt, the headers that say what a browser may do with the app's pages, a Content-Security-Policy among them, the logs, and metrics.
- The CLI
tug new,tug dev,tug gen,tug routes,tug lang,tug migrate,tug buildandtug key, in full.