web

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 29 Imported by: 0

README

web

A web framework for Vuka: routes, views, services and controllers are declared with decorators, parameters bind by name, and web.New() gathers everything into an app.

package main

import "github.com/vuka-lang/web"

@web.Service
type PetStore struct{ db *DB }            // built from its fields, a lazy singleton

@web.Get("/pets")
@web.Template("views/pets.templ")         // a templ component renders the result
func ListPets(store *PetStore) Result[PetList] { … }

@web.Get("/pets/{id}")
@web.Template("views/pet.html")           // html/template, the result as data
func ShowPet(id int, store *PetStore) Result[Pet] { return store.Find(id) }

@web.Get("/")
func Home(r *http.Request) vuka.Node {    // JSX
	return <Layout title="Home"><h1>Hi</h1></Layout>
}

@web.Post("/api/pets")
func CreatePet(in NewPet, store *PetStore) Result[Pet] { … }   // JSON in, JSON out

@web.Controller("/admin/pets")
type PetAdmin struct{ store *PetStore }

@web.Get("/")
func (a *PetAdmin) Index() Result[[]Pet] { return a.store.All() }

func main() {
	app := web.New()       // every route, service and controller declared at init
	app.Provide(OpenDB)    // more constructors, fx-style
	app.Run(":8080")       // graceful shutdown on SIGINT/SIGTERM
}

A complete app is in examples/pets.

go get github.com/vuka-lang/web

Declaring

@web.Get(path), Post, Put, Patch, Delete, Handle(method, path) a route; works on functions and controller methods
@web.Template(file [, name]) render the result with a view (below); before or after the route decorator
@web.Use(mw…) HTTP middleware for this route, func(http.Handler) http.Handler
@web.Service a struct becomes a dependency
@web.Controller(prefix) a struct becomes a dependency whose route methods are served under prefix

Paths use {name} for a segment and {name...} for the rest of the path, and match exactly (/pets doesn't match /pets/). Under a controller, "/" is the prefix itself.

Route decorators are Vuka declarers (func(*vuka.Decl)): they run once at init and record the function with its parameter names. Other decorators on the same function still wrap it — @audited below runs on every call.

The app

web.New(opts…) takes a snapshot of what has been declared, so several apps (in tests, say) each get their own container and singletons.

Option
web.Provide(ctors…) constructors for the container (also app.Provide)
web.Use(mw…) app-wide middleware (also app.Use); runs for every request, 404s included
web.WithRouter(r) another router (below)
web.Only(pkgs…) take only the declarations of these packages (import paths; "main")
web.Dev() show internal error messages; also VUKA_ENV=development
web.OnError(f) replace the error page / JSON error body

app.Build() resolves everything — every parameter of every route, every result and view, the whole dependency graph — and returns all problems at once, each naming the route, its source position and the parameter:

web: GET /pets (main.ListPets at pages.vuka:24): component PetsPage: main.PetList has no field for parameter title
web: GET /admin (main.Admin at admin.vuka:9): parameter db *sql.DB: no provider for *sql.DB, needed by main.Admin at admin.vuka:9
web: dependency cycle: *main.A -> *main.B -> *main.A

app.Handler() (an http.Handler to mount anywhere; it panics when Build fails), app.Start(ctx) and app.Run(addr) call Build first, so nothing is served from a broken app. Register everything before the first of them. app.Run("") listens on :$PORT, else :8080. web.Resolve[T](app) returns a dependency, for tests and main.

Parameters

Each parameter binds by its type and name:

parameter from
*http.Request, http.ResponseWriter, context.Context the request (the context is the request's)
a provided type the container
a scalar: string, ints, floats, bool, a encoding.TextUnmarshaler; *T of one (nil when absent), []T (repeated) the path segment of the same name, else the query parameter ?name=; absent → zero value
one struct, or pointer to a struct that isn't provided (with exported fields; else it is a missing dependency) the body: JSON for application/json, else the form (urlencoded, multipart) or query; then path segments into fields of the same name

Struct fields bind by their form tag, else json tag, else name, matched case-insensitively; validate:"required" rejects a zero value. A malformed value answers 400 with a message (id: "x" is not an integer).

A call decorator reaches the request through the handler's context.Context argument: web.Request(c.Context()), web.ResponseWriter(c.Context()).

decorator audited(c) {
	r := web.Request(c.Context())
	c.Next()
	log.Println(r.Method, r.URL.Path, c.Err())
}

@web.Delete("/{id}")
@web.Use(requireToken)
@audited
func (a *PetAdmin) Remove(ctx context.Context, id int) error { … }

Results

A handler returns nothing, error, T, (T, error) or Result[T].

result response
nothing, a nil error 204
vuka.Node (JSX, templ) HTML
T with @web.Template the view, HTML
T without JSON, 200
web.Redirect(url) (as a value or an error) 302 for GET/HEAD, else 303
*web.Error, wrapped or not its status and message
any other error 500; its message only in dev

Errors are an HTML page on HTML routes (a view, a vuka.Node or a redirect result) and {"error": …, "status": …} elsewhere. web.NotFound, Unauthorized, Forbidden, Conflict, web.BadRequest(msg), web.Status(code, format, args…); errors.Is(err, web.NotFound) matches by status. web.Redirect is also a vuka.Node, so a page handler can return it. Panics answer 500.

Views

templ. @web.Template("views/pets.templ") uses the file's one exported component; with several, name one: @web.Template("cards.templ", "Card"). The result fills the component's parameters: whole, when it has one parameter the result is assignable to; otherwise the result is a struct (or pointer to one) and each parameter takes the field of its name (exactly, or with the first letter's case changed: title ↔ Title). Layouts are templ's own composition (@Layout(title) { … }).

templ PetsPage(title string, pets []Pet) { @Layout(title) { … } }
type PetList struct { Title string; Pets []Pet }

html/template. Any other file is parsed once at Build and executed with the result; a second argument names a {{define}}d template to execute.

JSX / render functions. Return a vuka.Node; no view needed.

Views are embedded at compile time (vuka.File), so the binary carries them, from the handler's package directory or below it. A .templ file beside the handler is part of its package; one in a subfolder is that folder's package, as Go has it — views/pets.templ starts with package views — and Vuka compiles it and imports it for you:

examples/pets/
  pages.vuka        @web.Template("views/pets.templ"), @web.Template("views/pet.html")
  model/pet.go      package model: Pet, shared by handlers and views
  views/layout.templ  package views: Layout
  views/pets.templ    package views: PetsPage, composed with @Layout
  views/pet.html

Only the subfolder's exported components can be views, and its types can't come from the handler's package (package main can't be imported): put shared types in a package of their own, like model here. A JSX page uses a subfolder's component through its import, <views.Layout title="Home">.

Dependency injection

The container holds constructors — func(deps…) T or func(deps…) (T, error) — and ready values (anything that isn't a function). @web.Service and @web.Controller structs are constructed from their fields (Vuka's vuka.Type.New; tag a field inject:"-" to leave it out). Every value is a lazy singleton per app; dependencies match by exact type (provide a constructor returning the interface to inject an interface). Missing providers, cycles and duplicates are Build errors.

A provided value implementing Start(context.Context) error is started by app.Start/Run in dependency order; Stop(context.Context) error runs on shutdown in reverse order.

Router

The default router is net/http's ServeMux with Go 1.22 patterns. Any router plugs in through

type Router interface {
	http.Handler
	Handle(method, pattern string, h http.Handler)   // canonical {name} / {name...} patterns
	Param(r *http.Request, name string) string
}

with web.New(web.WithRouter(r)). A chi adapter is a dozen lines (Handle → r.Method(method, pattern, h), Param → chi.URLParam); the tests carry a tiny one.

Static files

app.Static("/assets", fsys) serves an fs.FS (an embed.FS sub-tree, os.DirFS(…)).

Plain Go

Everything works from Go without decorators. Reflection can't see parameter names, so scalar parameters need web.Params; structs, request values and dependencies bind as usual.

app := web.New(web.Provide(NewStore))
app.Get("/pets/{id}", func(s *Store, id int) (Pet, error) { … }, web.Params("s", "id"))
app.Post("/pets", func(in NewPet, s *Store) (Pet, error) { … })
app.Get("/page", page, web.View(vuka.File("views/page.html")), web.Wrap(mw))

From Go, vuka.File("…") reads from disk relative to the working directory.

Testing

go test ./...                     # the package
vuka test ./...                   # also examples/ and internal/vukatest, written in Vuka

Limits

  • One struct parameter per handler; scalars bind from the path or query only (use *http.Request for headers and cookies).
  • Views live in the handler's package directory or below it (go:embed doesn't reach up); ../shared/x.templ is an error.
  • A vuka.File literal is embedded only where a decorator or attribute takes one, so there is no web.Layout(file) option; compose layouts in templ, or {{define}} them in one html file.
  • Dependencies match by exact type; no named values or groups.
  • Status codes for success are fixed (200/204); write through http.ResponseWriter for anything else.

License

MIT

Documentation

Overview

Package web is a web framework for Vuka: routes, views and services are declared with decorators, and web.New gathers them into an app.

@web.Get("/pets/{id}")
@web.Template("pet.html")
func ShowPet(id int, store *PetStore) Result[Pet] { … }

func main() { web.New().Run(":8080") }

Everything a decorator declares can also be registered from plain Go (App.Get, App.Provide, …); see the README.

Index

Constants

This section is empty.

Variables

View Source
var (
	NotFound     = &Error{Status: http.StatusNotFound, Message: "not found"}
	Unauthorized = &Error{Status: http.StatusUnauthorized, Message: "unauthorized"}
	Forbidden    = &Error{Status: http.StatusForbidden, Message: "forbidden"}
	Conflict     = &Error{Status: http.StatusConflict, Message: "conflict"}
)

Functions

func Controller

func Controller(prefix string) func(*vuka.Type)

Controller makes a struct a service whose route methods are served under prefix.

func Delete

func Delete(path string) func(*vuka.Decl)

func Get

func Get(path string) func(*vuka.Decl)

func Handle

func Handle(method, path string) func(*vuka.Decl)

Handle declares a route for method and path: @web.Handle("OPTIONS", "/x"). Path segments in braces bind parameters of the same name: "/pets/{id}"; "{rest...}" matches the rest of the path.

func Patch

func Patch(path string) func(*vuka.Decl)

func Post

func Post(path string) func(*vuka.Decl)

func Put

func Put(path string) func(*vuka.Decl)

func Request

func Request(ctx context.Context) *http.Request

Request is the request being served, from a context.Context a handler received: call decorators reach it through c.Context().

func Resolve

func Resolve[T any](a *App) (T, error)

Resolve returns the app's T, building it and its dependencies.

func ResponseWriter

func ResponseWriter(ctx context.Context) http.ResponseWriter

ResponseWriter is the response being written, from a handler's context.

func Service

func Service(t *vuka.Type)

Service makes a struct a dependency: a lazy singleton built from its fields, each injected by type.

func Template

func Template(f vuka.File, name ...string) func(*vuka.Decl)

Template renders a route's result with a view: a .templ file of the same package (its one exported component, or the one named) or an html/template file (its main template, or the one named). The order of @web.Template and the route's method decorator doesn't matter.

Types

type App

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

App is a web application: the routes, services and controllers declared with decorators, and whatever was added in code.

func New

func New(opts ...Option) *App

New makes an app of every route, service and controller declared so far (each app takes its own snapshot, with its own singletons).

func (*App) Build

func (a *App) Build() error

Build resolves the app: every route's parameters, results and view, and the dependency graph. It reports every problem at once, before anything is served; Handler, Start and Run call it.

func (*App) Delete

func (a *App) Delete(path string, fn any, opts ...RouteOption) *App

func (*App) Get

func (a *App) Get(path string, fn any, opts ...RouteOption) *App

func (*App) Handle

func (a *App) Handle(method, path string, fn any, opts ...RouteOption) *App

Handle registers fn for method and path, bound as a decorated handler is.

func (*App) Handler

func (a *App) Handler() http.Handler

Handler is the app as an http.Handler, to serve or mount anywhere. It panics when Build fails.

func (*App) Patch

func (a *App) Patch(path string, fn any, opts ...RouteOption) *App

func (*App) Post

func (a *App) Post(path string, fn any, opts ...RouteOption) *App

func (*App) Provide

func (a *App) Provide(ctors ...any) *App

Provide adds constructors (or ready values) to the app's container.

func (*App) Put

func (a *App) Put(path string, fn any, opts ...RouteOption) *App

func (*App) Run

func (a *App) Run(addr string) error

Run serves the app on addr (":" + $PORT, else ":8080", when empty) until SIGINT or SIGTERM, then shuts down gracefully and stops the services.

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start builds every dependency the routes need, then calls Start on each provided value implementing Starter, in dependency order.

func (*App) Static

func (a *App) Static(prefix string, fsys fs.FS) *App

Static serves the files of fsys under prefix.

func (*App) Stop

func (a *App) Stop(ctx context.Context) error

Stop calls Stop on each built value implementing Stopper, in reverse order of construction.

func (*App) Use

func (a *App) Use(mw ...Middleware) *App

Use adds app-wide middleware; the first added runs first.

type Error

type Error struct {
	Status  int
	Message string
	Err     error // the cause, never shown
}

Error is an error with an HTTP status. Returned by a handler (wrapped or not), it answers with that status and message: an HTML page for an HTML route, JSON otherwise.

func BadRequest

func BadRequest(msg string) *Error

BadRequest is a 400 error with msg.

func Status

func Status(status int, format string, args ...any) *Error

Status is an error answering with status and the formatted message.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches another *Error of the same status: errors.Is(err, web.NotFound).

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Middleware

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

Middleware wraps an http.Handler.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures an App.

func Dev

func Dev() Option

Dev shows the messages of internal errors in responses; so does the environment variable VUKA_ENV=development.

func OnError

func OnError(f func(w http.ResponseWriter, r *http.Request, e *Error, html bool)) Option

OnError replaces the error page and JSON error body.

func Only

func Only(pkgs ...string) Option

Only limits the declared routes, services and controllers an app takes to those of the given packages (import paths; "main" for the main package).

func Provide

func Provide(ctors ...any) Option

Provide adds constructors to the app's container: func(deps…) T or func(deps…) (T, error). A value that isn't a function is provided as is.

func WithRouter

func WithRouter(r Router) Option

WithRouter serves the app's routes with r instead of net/http's ServeMux.

type Redirect

type Redirect string

Redirect, returned by a handler as its result or as its error, redirects to the URL: 302 for GET and HEAD, 303 See Other otherwise. It is also a vuka.Node, so a handler returning a page can redirect instead.

func (Redirect) Error

func (r Redirect) Error() string

func (Redirect) Render

func (r Redirect) Render(context.Context, io.Writer) error

type RouteOption

type RouteOption func(*routeSpec)

RouteOption configures a route registered in code.

func Params

func Params(names ...string) RouteOption

Params names a handler's parameters in order, which reflection can't see: scalar parameters bind by name.

func View

func View(f vuka.File, name ...string) RouteOption

View renders the route's result with a view, as @web.Template does.

func Wrap

func Wrap(mw ...Middleware) RouteOption

Wrap adds middleware to the route, as @web.Use does.

type Router

type Router interface {
	http.Handler
	Handle(method, pattern string, h http.Handler)
	Param(r *http.Request, name string) string
}

Router matches requests to routes. Patterns use the canonical syntax: "/pets/{id}" binds a segment, "/files/{path...}" the rest of the path, and a pattern matches its path exactly. Param returns a bound value for a request the router dispatched.

type ServeMux

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

ServeMux is the default Router: net/http's ServeMux with Go 1.22 patterns.

func NewServeMux

func NewServeMux() *ServeMux

func (*ServeMux) Handle

func (m *ServeMux) Handle(method, pattern string, h http.Handler)

func (*ServeMux) Param

func (m *ServeMux) Param(r *http.Request, name string) string

func (*ServeMux) ServeHTTP

func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Starter

type Starter interface{ Start(context.Context) error }

Starter is a provided value with work to do when the app starts: Run calls Start in dependency order.

type Stopper

type Stopper interface{ Stop(context.Context) error }

Stopper is a provided value with work to do when the app stops: Stop is called in the reverse order of construction.

type Uses

type Uses func(*vuka.Decl)

Uses is what Use returns: a declarer adding middleware to one route, and an Option adding it to the whole app.

func Use

func Use(mw ...Middleware) Uses

Use adds HTTP middleware, the first given outermost: @web.Use(mw) on a route, or web.New(web.Use(mw)) for every request of the app.

Directories

Path Synopsis
examples
pets/model
Package model holds the pets app's data types, shared by its handlers and its views.
Package model holds the pets app's data types, shared by its handlers and its views.

Jump to

Keyboard shortcuts

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