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

vial

package module
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 21 Imported by: 0

README

[!WARNING] This is a learning/pet project in which I have used AI. If you are uncomfortable with the use of AI this is not the place for you. Of course there will be slop in places I tried to erradicate the most of it,but surely there is. My solely goal with this project is to level up in Go and create a light and easy framework to create web apps. If you feel this suits your needs feel free to use it and contribute to the project

vial

CI

vial is an early Go web-framework MVP focused on two things:

  1. A small, explicit HTTP API built directly on net/http.
  2. A fast development loop that rebuilds and restarts the application after source changes.

Current MVP

  • Error-returning handlers: func(*vial.Context) error
  • Standard Go middleware composition
  • Named, method-aware routes and path parameters through http.ServeMux
  • Route groups with scoped middleware
  • Named application modules with isolated route registrars
  • JSON, text, redirects, and empty responses
  • Cached path, query, header, cookie, form, multipart, and JSON binding
  • Centralized HTTP errors and transport-neutral application faults
  • Request IDs, structured logging, panic recovery, and configurable CORS
  • Graceful HTTP shutdown
  • Raw http.Handler mounting
  • Standard httptest compatibility
  • vial dev automatic build-and-restart loop
  • vial load bounded HTTP load checks and CI thresholds
  • Last-known-good process remains online after compilation failures
  • No runtime dependencies outside the Go standard library

Run it

The module lives at github.com/jrgf/go-vial.

git clone https://github.com/jrgf/go-vial.git
cd go-vial

make install
vial version

go test ./...
vial dev ./examples/hello

make install places vial in Go's binary directory (GOBIN, or $GOPATH/bin by default), which must be on PATH.

Open:

http://localhost:8080
http://localhost:8080/users/42

Change the response in examples/hello/main.go and save. The development runner will build a candidate binary, gracefully stop the previous application only after compilation succeeds, and start the replacement.

A failed build behaves like this:

[vial] source change detected; rebuilding
[vial] building ./examples/hello
examples/hello/main.go:20:2: undefined: broken
[vial] build failed; last successful application remains running

Minimal application

package main

import (
    "context"
    "log"
    "net/http"

    "github.com/jrgf/go-vial"
    "github.com/jrgf/go-vial/middleware"
)

func main() {
    app := vial.New(
        vial.WithDisallowUnknownJSONFields(true),
    )

    app.Use(
        middleware.RequestID(),
        middleware.Logger(),
        middleware.Recover(),
    )

    app.Get("/", func(c *vial.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "message": "Hello from vial",
        })
    })

    app.Get("/users/{id}", func(c *vial.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "id": c.Param("id"),
        })
    })

    if err := app.Run(context.Background(), ":8080"); err != nil {
        log.Fatal(err)
    }
}

App implements http.Handler, so it also works without the framework server helper:

server := &http.Server{
    Addr:    ":8080",
    Handler: app,
}

Route groups

api := app.Group("/api")
api.Use(authenticationMiddleware)

api.Get("/users/{id}", getUser)
api.Post("/users", createUser)

Application middleware wraps all requests, including 404 and 405 responses. Group middleware wraps only endpoints registered through that group.

JSON binding

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

app.Post("/users", func(c *vial.Context) error {
    var request CreateUserRequest
    if err := c.BindJSON(&request); err != nil {
        return err
    }

    return c.JSON(http.StatusCreated, request)
})

The default body limit is 16 MiB. Applications can override it and reject unknown JSON fields:

app := vial.New(
	vial.WithMaxBodySize(10 << 20),
    vial.WithDisallowUnknownJSONFields(true),
)

Context.Bind combines fields tagged with path, query, header, cookie, and form with JSON or form bodies. Uploaded files use Context.FormFile; see the runnable examples/upload.

HTML templates

Keep parsing in html/template; render buffers the result before committing the response:

parsed, err := template.ParseFS(webFS, "templates/*.html")
if err != nil {
    return err
}
views := render.New(parsed)

app.Get("/", func(c *vial.Context) error {
    return views.HTML(c, http.StatusOK, "home", page)
})

The runnable examples/web adds a validated, CSRF-protected form and uses embed.FS with the native http.FileServerFS for static assets:

VIAL_ALLOW_INSECURE_COOKIE=1 vial dev ./examples/web

The insecure-cookie flag is only for local HTTP.

Vial leaves session policy opt-in. The isolated examples/securecookie module demonstrates signed sessions, key rotation, and one-time flash messages with securecookie:

umask 077
openssl rand -hex 32 > /tmp/vial-session-keys
SESSION_KEYS_FILE=/tmp/vial-session-keys VIAL_ALLOW_INSECURE_COOKIE=1 vial dev ./examples/securecookie

The example reloads this file every minute and expires sessions after five minutes. Rotate with NEW_KEY,OLD_KEY, newest first; remove the old key after five minutes.

VIAL_ALLOW_INSECURE_COOKIE=1 is only for local HTTP. Cookie contents are authenticated but not encrypted, so never store secrets in them. SameSite is defense in depth, not a replacement for CSRF protection.

Testing

testkit runs the full application lifecycle and cleans up automatically:

server := testkit.Start(t, app)
response := server.JSON(http.MethodPost, "/users", request)
response.RequireStatus(http.StatusCreated)

var created User
response.Decode(&created)

Raw net/http requests and httptest remain supported.

Background tasks

Keep shared queues application-owned; use contexts only for cancellation:

type Event struct {
    Name string
}

events := make(chan Event, 128)

app.Post("/events", func(c *vial.Context) error {
    select {
    case events <- Event{Name: "user.created"}:
        return c.NoContent(http.StatusAccepted)
    case <-c.Request().Context().Done():
        return c.Request().Context().Err()
    default:
        return vial.NewHTTPError(503, "queue_full", "Event queue is full")
    }
})

app.Go("event-consumer", func(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case event := <-events:
            if err := handleEvent(ctx, event); err != nil {
                return err
            }
        }
    }
})

This queue is in-memory and loses pending events when the process stops. Use a database outbox or external broker when events must be durable. See the runnable examples/events.

Configuration

Use config.Load with an initialized struct. Existing values are defaults, JSON files are applied in order, and environment variables win:

type AppConfig struct {
    Environment string `json:"environment" env:"VIAL_ENV"`
    HTTP        config.HTTP `json:"http"`
}

func (configuration *AppConfig) Validate() error {
    if configuration.Environment == "" {
        return errors.New("environment is required")
    }
    return nil
}

configuration := AppConfig{Environment: "development"}

if err := config.Load(
    &configuration,
    config.OptionalFile("config.json"),
); err != nil {
    log.Fatal(err)
}

if err := app.Run(context.Background(), configuration.HTTP.Address()); err != nil {
    log.Fatal(err)
}

An optional config.json can override the defaults:

{
  "http": {
    "port": 9000
  }
}

Environment variables take final precedence:

VIAL_HTTP_PORT=9090 go run .

Use config.File when a file is required. Configuration types may implement config.Validator; validation runs after all sources are loaded.

config.HTTP.Address() defaults to 127.0.0.1:8080. Production deployments that must accept external traffic can set VIAL_HTTP_HOST=0.0.0.0 explicitly. See the runnable examples/config application.

Errors

Handlers return an error. Business failures use transport-neutral faults:

return fault.New(
    fault.NotFound,
    "user_not_found",
    "The requested user was not found",
)

Use HTTPError only when an HTTP-specific status or response header is needed.

Default response:

{
  "error": {
    "code": "user_not_found",
    "message": "The requested user was not found"
  }
}

Unexpected errors are rendered as a generic 500 response. Their internal details remain in server logs. Unmatched routes and unsupported methods use the same error handler and return not_found and method_not_allowed codes; 405 responses include Allow.

Mount standard handlers

app.HandleHTTP("GET /health", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
    w.WriteHeader(http.StatusNoContent)
}))

This is the primary escape hatch for existing middleware, metrics endpoints, profilers, and other net/http integrations.

Development runner

Build the CLI:

go install ./cmd/vial

Usage:

vial dev [flags] [package] [-- application arguments]

Examples:

vial dev ./examples/hello
vial dev --verbose ./cmd/server
vial dev --debounce 500ms ./cmd/server
vial dev --exclude generated ./cmd/server
vial dev ./cmd/server -- --config ./config/dev.json

Flags:

Flag Default Meaning
--root current directory Directory to watch and use as the build working directory
--debounce 250ms Quiet period before rebuilding
--restart-timeout 3s Time allowed for graceful child shutdown
--exclude none Additional ignored name or path; repeatable
--verbose false Print each relevant changed path

The MVP watcher recursively scans:

  • *.go
  • go.mod
  • go.sum
  • go.work
  • go.work.sum

It ignores:

  • .git
  • .vial
  • vendor
  • node_modules
  • tmp
  • dist

Generated binaries are written under .vial/bin and are ignored by the watcher.

Inspect routes without starting the server:

vial routes ./examples/hello
vial routes --json ./examples/hello

This command supports applications that call App.Run; applications using a custom http.Server can call App.Routes directly.

Validate configuration and application build setup without starting the server:

vial doctor ./examples/config

Run a bounded load check against a deployed endpoint:

vial load --workers 2000 --duration 30s --timeout 5s http://localhost:8080/
vial load --max-error-rate 1 --max-p95 250ms http://localhost:8080/
Replacement sequence
source change
    ↓
debounce
    ↓
build unique candidate binary
    ├── failure → retain current process
    └── success
            ↓
       interrupt current process
            ↓
       wait up to restart timeout
            ↓
       start candidate

Builds never run concurrently. Changes detected during a build remain queued for the next pass.

Repository layout

.
├── app.go                 # application, registration, and net/http adapter
├── context.go             # request and response helpers
├── binding.go             # query, form, multipart, and JSON body limits
├── errors.go              # HTTP error model and renderer
├── server.go              # server lifecycle and graceful shutdown
├── middleware/            # request ID, logging, recovery, CORS, and CSRF
├── internal/dev/          # watcher, builder, runner, and process control
├── cmd/vial/              # development and load-check CLI
└── examples/              # runnable applications

Verification

go test ./...
go test -race ./...
go vet ./...

The codebase is also compile-checked for Windows and macOS in CI.

Known MVP limitations

  • Development change detection uses recursive polling rather than native filesystem events.
  • Only Go source and module/workspace files trigger rebuilds.
  • Windows child replacement uses direct termination; graceful console-event delivery is a later enhancement.
  • Route registration becomes immutable after the application is built or first served.

Contributing

See CONTRIBUTING.md for development setup, quality requirements, and pull request guidance.

License

MIT

Documentation

Overview

Package vial provides a small, net/http-compatible web application kernel with error-returning handlers, middleware, request helpers, and graceful serving.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StatusCode

func StatusCode(err error) int

StatusCode returns the HTTP status represented by err, defaulting to 500.

Types

type App

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

App is the framework application and implements http.Handler.

func New

func New(options ...Option) *App

func (*App) Build

func (app *App) Build() error

Build validates and freezes application registrations.

func (*App) Delete

func (app *App) Delete(path string, handler Handler, options ...RouteOption)

func (*App) Get

func (app *App) Get(path string, handler Handler, options ...RouteOption)

func (*App) Go added in v0.7.0

func (app *App) Go(name string, task Task, options ...TaskOption)

Go registers a named background task. Tasks are critical by default and begin running when the application starts.

func (*App) Group

func (app *App) Group(prefix string) *Group

func (*App) Handle

func (app *App) Handle(method, path string, handler Handler, options ...RouteOption)

func (*App) HandleHTTP

func (app *App) HandleHTTP(pattern string, handler http.Handler, options ...RouteOption)

HandleHTTP mounts a standard-library handler using a native ServeMux pattern. Examples include "/metrics", "GET /health", and "api.example.com/".

func (*App) OnStart added in v0.4.0

func (app *App) OnStart(hooks ...LifecycleHook)

OnStart registers hooks that run in registration order before components start.

func (*App) OnStop added in v0.4.0

func (app *App) OnStop(hooks ...LifecycleHook)

OnStop registers hooks that run in reverse registration order after components stop.

func (*App) Options

func (app *App) Options(path string, handler Handler, options ...RouteOption)

func (*App) Patch

func (app *App) Patch(path string, handler Handler, options ...RouteOption)

func (*App) Post

func (app *App) Post(path string, handler Handler, options ...RouteOption)

func (*App) Put

func (app *App) Put(path string, handler Handler, options ...RouteOption)

func (*App) Register added in v0.4.0

func (app *App) Register(modules ...Module) error

Register stages modules and commits their routes atomically.

func (*App) Routes added in v0.2.0

func (app *App) Routes() ([]Route, error)

Routes validates the application and returns registered route metadata in registration order. The returned slice does not share state with the app.

func (*App) Run

func (app *App) Run(contextValue context.Context, address string) error

Run listens on address and serves until the parent context or a supported OS shutdown signal is received.

func (*App) Serve

func (app *App) Serve(contextValue context.Context, listener net.Listener) (serveErr error)

Serve runs the application on an existing listener. This is useful for tests and for callers that need custom listener configuration.

func (*App) ServeHTTP

func (app *App) ServeHTTP(writer http.ResponseWriter, request *http.Request)

func (*App) SetErrorHandler

func (app *App) SetErrorHandler(handler ErrorHandler)

func (*App) Use

func (app *App) Use(middleware ...Middleware)

type BindingError added in v0.4.0

type BindingError struct {
	Source string
	Field  string
	Cause  error
}

BindingError identifies the request field that could not be bound.

func (*BindingError) Error added in v0.4.0

func (err *BindingError) Error() string

func (*BindingError) Unwrap added in v0.4.0

func (err *BindingError) Unwrap() error

type Context

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

Context contains request-scoped framework state while retaining direct access to the standard net/http request and response writer.

func (*Context) App

func (context *Context) App() *App

func (*Context) Bind added in v0.4.0

func (context *Context) Bind(destination any) error

Bind combines tagged path, query, header, cookie, JSON, and form input.

func (*Context) BindCookie added in v0.4.0

func (context *Context) BindCookie(destination any) error

BindCookie binds request cookies to fields tagged with `cookie`.

func (*Context) BindForm added in v0.3.0

func (context *Context) BindForm(destination any) error

BindForm binds URL-encoded or multipart form fields to primitive struct fields tagged with `form`. Repeated values bind to slices.

func (*Context) BindHeader added in v0.4.0

func (context *Context) BindHeader(destination any) error

BindHeader binds request headers to fields tagged with `header`.

func (*Context) BindJSON

func (context *Context) BindJSON(destination any) error

BindJSON decodes a single JSON value into destination using the application's body limit and unknown-field policy.

func (*Context) BindPath added in v0.4.0

func (context *Context) BindPath(destination any) error

BindPath binds route parameters to fields tagged with `path`.

func (*Context) BindQuery added in v0.3.0

func (context *Context) BindQuery(destination any) error

BindQuery binds query parameters to primitive struct fields tagged with `query`. Repeated values bind to slices.

func (*Context) BytesWritten

func (context *Context) BytesWritten() int64

func (*Context) Committed

func (context *Context) Committed() bool

func (*Context) FormFile added in v0.3.0

func (context *Context) FormFile(name string) (multipart.File, *multipart.FileHeader, error)

FormFile returns the first uploaded file for name. The caller must close the file. Vial removes any temporary multipart files after the request finishes.

func (*Context) FormValue added in v0.10.0

func (context *Context) FormValue(name string) (string, error)

FormValue returns the first URL-encoded or multipart form value for name.

func (*Context) Get

func (context *Context) Get(key string) (any, bool)

func (*Context) Header

func (context *Context) Header(name string) string

func (*Context) JSON

func (context *Context) JSON(status int, value any) error

func (*Context) Logger

func (context *Context) Logger() *slog.Logger

func (*Context) NoContent

func (context *Context) NoContent(status int) error

func (*Context) Param

func (context *Context) Param(name string) string

func (*Context) Query

func (context *Context) Query(name string) string

func (*Context) Redirect

func (context *Context) Redirect(status int, location string) error

func (*Context) Request

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

func (*Context) Response

func (context *Context) Response() http.ResponseWriter

func (*Context) Route

func (context *Context) Route() *Route

func (*Context) Set

func (context *Context) Set(key string, value any)

func (*Context) SetLogger

func (context *Context) SetLogger(logger *slog.Logger)

func (*Context) Status

func (context *Context) Status() int

func (*Context) Text

func (context *Context) Text(status int, value string) error

type ErrorHandler

type ErrorHandler func(*Context, error)

ErrorHandler renders errors returned by handlers or middleware.

type Group

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

Group applies a common path prefix and middleware chain to related routes.

func (*Group) Delete

func (group *Group) Delete(path string, handler Handler, options ...RouteOption)

func (*Group) Get

func (group *Group) Get(path string, handler Handler, options ...RouteOption)

func (*Group) Group

func (group *Group) Group(prefix string) *Group

func (*Group) Handle

func (group *Group) Handle(method, path string, handler Handler, options ...RouteOption)

func (*Group) Options

func (group *Group) Options(path string, handler Handler, options ...RouteOption)

func (*Group) Patch

func (group *Group) Patch(path string, handler Handler, options ...RouteOption)

func (*Group) Post

func (group *Group) Post(path string, handler Handler, options ...RouteOption)

func (*Group) Put

func (group *Group) Put(path string, handler Handler, options ...RouteOption)

func (*Group) Use

func (group *Group) Use(middleware ...Middleware)

type HTTPError

type HTTPError struct {
	Status  int
	Code    string
	Message string
	Cause   error
	Headers http.Header
}

HTTPError is an application error with a stable HTTP representation.

func BadRequest

func BadRequest(code, message string) *HTTPError

func Conflict

func Conflict(code, message string) *HTTPError

func Forbidden

func Forbidden(code, message string) *HTTPError

func InternalServerError

func InternalServerError(cause error) *HTTPError

func MethodNotAllowed added in v0.3.0

func MethodNotAllowed(code, message string) *HTTPError

func NewHTTPError

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

func NotFound

func NotFound(code, message string) *HTTPError

func RequestEntityTooLarge

func RequestEntityTooLarge(code, message string) *HTTPError

func Unauthorized

func Unauthorized(code, message string) *HTTPError

func UnsupportedMediaType

func UnsupportedMediaType(code, message string) *HTTPError

func WrapHTTPError

func WrapHTTPError(status int, code, message string, cause error) *HTTPError

func (*HTTPError) Error

func (err *HTTPError) Error() string

func (*HTTPError) Unwrap

func (err *HTTPError) Unwrap() error

type Handler

type Handler func(*Context) error

Handler is the framework's HTTP handler contract. Returning errors keeps transport error rendering centralized and makes middleware composition simple.

type LifecycleHook added in v0.4.0

type LifecycleHook func(context.Context) error

LifecycleHook runs while the application starts or stops.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler with cross-cutting behavior.

type Module added in v0.4.0

type Module interface {
	Name() string
	Register(*Registrar) error
}

Module registers one named application capability.

type Option

type Option func(*config)

Option configures an App.

func WithDisallowUnknownJSONFields

func WithDisallowUnknownJSONFields(enabled bool) Option

func WithIdleTimeout

func WithIdleTimeout(timeout time.Duration) Option

func WithLogger

func WithLogger(logger *slog.Logger) Option

func WithMaxBodySize

func WithMaxBodySize(bytes int64) Option

func WithReadHeaderTimeout

func WithReadHeaderTimeout(timeout time.Duration) Option

func WithReadTimeout

func WithReadTimeout(timeout time.Duration) Option

func WithShutdownTimeout

func WithShutdownTimeout(timeout time.Duration) Option

func WithWriteTimeout

func WithWriteTimeout(timeout time.Duration) Option

type Registrar added in v0.4.0

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

Registrar exposes the route registration surface available to modules.

func (*Registrar) Group added in v0.4.0

func (registrar *Registrar) Group(prefix string) *Group

Group creates a module route group.

func (*Registrar) Handle added in v0.4.0

func (registrar *Registrar) Handle(method, path string, handler Handler, options ...RouteOption)

Handle registers a module route.

func (*Registrar) HandleHTTP added in v0.4.0

func (registrar *Registrar) HandleHTTP(pattern string, handler http.Handler, options ...RouteOption)

HandleHTTP mounts a standard-library handler in the module.

func (*Registrar) Use added in v0.4.0

func (registrar *Registrar) Use(middleware ...Middleware)

Use adds middleware to every route registered by the module.

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

ResponseWriter tracks response state while retaining access to the underlying standard-library writer through Unwrap.

func (*ResponseWriter) BytesWritten

func (writer *ResponseWriter) BytesWritten() int64

func (*ResponseWriter) Committed

func (writer *ResponseWriter) Committed() bool

func (*ResponseWriter) Flush

func (writer *ResponseWriter) Flush()

func (*ResponseWriter) Hijack

func (writer *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)

func (*ResponseWriter) Push

func (writer *ResponseWriter) Push(target string, options *http.PushOptions) error

func (*ResponseWriter) ReadFrom

func (writer *ResponseWriter) ReadFrom(reader io.Reader) (int64, error)

func (*ResponseWriter) Status

func (writer *ResponseWriter) Status() int

func (*ResponseWriter) Unwrap

func (writer *ResponseWriter) Unwrap() http.ResponseWriter

func (*ResponseWriter) Write

func (writer *ResponseWriter) Write(data []byte) (int, error)

func (*ResponseWriter) WriteHeader

func (writer *ResponseWriter) WriteHeader(status int)

type Route

type Route struct {
	Method  string
	Path    string
	Pattern string
	Name    string
	Module  string
}

Route contains metadata for a registered endpoint.

type RouteOption added in v0.4.0

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

RouteOption configures route metadata.

func RouteName added in v0.4.0

func RouteName(name string) RouteOption

RouteName assigns a stable name to a route.

type Task added in v0.7.0

type Task func(context.Context) error

Task is application work that runs until its context is canceled or it fails.

type TaskOption added in v0.7.0

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

TaskOption configures a background task.

func NonCritical added in v0.7.0

func NonCritical() TaskOption

NonCritical keeps the application running when the task exits or fails.

type Validator added in v0.10.0

type Validator interface {
	Validate() error
}

Validator validates bound request data. Returning a *fault.Error preserves its field errors for response handling and rendering.

Directories

Path Synopsis
cmd
vial command
Package config loads typed application configuration from explicit files and environment variables.
Package config loads typed application configuration from explicit files and environment variables.
examples
config command
events command
hello command
json-api command
modules command
tasks command
upload command
web command
Package fault defines transport-neutral application errors.
Package fault defines transport-neutral application errors.
internal
dev
load
Package load runs bounded HTTP load checks for the vial command.
Package load runs bounded HTTP load checks for the vial command.
Package middleware contains the framework's optional default HTTP middleware.
Package middleware contains the framework's optional default HTTP middleware.
Package render writes html/template output through Vial handlers.
Package render writes html/template output through Vial handlers.
Package testkit provides lifecycle-aware HTTP helpers for Vial applications.
Package testkit provides lifecycle-aware HTTP helpers for Vial applications.
vialgrpc module
vialws module

Jump to

Keyboard shortcuts

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