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

vial

package module
v0.3.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: 20 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

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
  • Method-aware routes and path parameters through http.ServeMux
  • Route groups with scoped middleware
  • JSON, text, redirects, and empty responses
  • Query, form, multipart, and strict JSON binding with body limits
  • Centralized structured error responses
  • 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
  • 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),
)

Query and form structs use query and form tags. Uploaded files use Context.FormFile; see the runnable examples/upload.

Errors

Handlers return an error. Known application failures use HTTPError helpers:

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

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.

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, and CORS
├── internal/dev/          # watcher, builder, runner, and process control
├── cmd/vial/                # development 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 registrations and freezes the application routing graph.

func (*App) Delete

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

func (*App) Get

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

func (*App) Group

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

func (*App) Handle

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

func (*App) HandleHTTP

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

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

func (*App) Options

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

func (*App) Patch

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

func (*App) Post

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

func (*App) Put

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

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

func (*Group) Get

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

func (*Group) Group

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

func (*Group) Handle

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

func (*Group) Options

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

func (*Group) Patch

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

func (*Group) Post

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

func (*Group) Put

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

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 Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler with cross-cutting behavior.

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

Route contains metadata for a registered endpoint.

Directories

Path Synopsis
cmd
vial command
examples
hello command
json-api command
upload command
internal
dev
Package middleware contains the framework's optional default HTTP middleware.
Package middleware contains the framework's optional default HTTP middleware.
vialgrpc module
vialws module

Jump to

Keyboard shortcuts

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