Build web apps in Go, at ease
Routing, data, accounts, queues, mail, storage, search, AI and translations in one framework, built on net/http. One binary runs it all.
From zero to an app with accounts
go install anetos.dev/anetos/cli/cmd/anetos@latest
anetos new blog && cd blog # templ views, sessions, CSRF, SQLite by default
go tool anetos make:auth # accounts: password, Google, GitHub, API tokens
go run . migrate
go tool anetos dev # rebuild and reload on every change
go build -o blog . && ./blog # web, queue workers and the scheduler, in one binaryBatteries included, Go all the way
Typed handlers and models, generated code instead of reflection at run time, and plain net/http underneath: the comfort of Laravel or Rails, without leaving Go's way of doing things.
HTTP that stays net/http
A router, typed handlers that bind and validate their input, middleware, sessions, CSRF and htmx-friendly views.
Data, typed
A query builder with generated columns, relations without N+1, migrations and seeders on PostgreSQL, MySQL, MariaDB and SQLite.
Accounts and permissions
Sign-in with passwords, Google and GitHub, API tokens, email verification, and roles per app or per team.
Work in the background
Queues, events, pub/sub listeners and a scheduler, supervised in the same binary — or split by role when you scale.
AI in the app
Typed model calls, tools that act as the user, streamed answers, stored conversations and budgets: Anthropic, OpenAI and Gemini.
Search
Full-text, vector and hybrid search in the database you already run, with no separate search server.
Every language, every zone
Translations in YAML, the visitor’s language from the URL or their settings, dates and prices in their locale, times stored in UTC.
Mail and files
Templated mail over SMTP or Postmark, and file storage on disk, S3 or Google Cloud Storage with signed URLs.
Tests that read like specs
A test client, fakes for mail, queues, AI and the clock, and database helpers — no server to start.
Handlers that say what they take
Input structs say where each value comes from and how it's checked. The handler gets it ready to use, and returns typed output or an error that becomes the right HTTP response.
// CreateNote binds from JSON or a form; the rules run before the handler.
type CreateNote struct {
Title string `json:"title" validate:"required|max:200"`
Body string `json:"body" validate:"max:10000"`
Tags []string `json:"tags" validate:"max:5|distinct|alpha_dash"`
}
func (h *Notes) Store(c *web.Ctx, in CreateNote) (web.Responder, error) {
note := &Note{Title: in.Title, Body: in.Body, Tags: in.Tags}
if err := db.Create(c, note); err != nil {
return nil, err
}
return web.Created(note), nil // 201, as JSON
}
r.Post("/notes", web.H(h.Store)).Name("notes.store")