Muzak for Go 1.27 · v0.3.1

The signature
is the contract.

Muzak is the type-safe web framework for Go. A handler's input type is the request and its return type is the response, both checked when you compile. Built on net/http, with no third-party dependencies at all.

v0.3.1 · pre-1.0Go 1.270 dependencies99.1% covered
zsh · install
$ go get muzak.dev/framework
go: downloading muzak.dev/framework v0.3.1
go: added muzak.dev/framework v0.3.1
✓ no third party dependencies
$ go run ./cmd
14:32:07.482 INFO [Server] Starting Muzak application...
14:32:07.514 INFO [Server] Listening on :8080
$ ▍

One module, named muzak. Routing, validation, DI and OpenAPI are in it.

Installation

One module, then a running API

Add the module, lay the project out so cmd/main.go does nothing but compose, and run it. Each card below plays a real command and the output Muzak actually prints, from the first request to a graceful shutdown.

installgo get
$ go version
go version go1.27.0 darwin/arm64
$ go get muzak.dev/framework
✓ added muzak.dev/framework v0.3.1
layoutcmd/main.go
$ tree -L 2 awesome-api
awesome-api/
├─ cmd/main.go composition
├─ core/ config, dependencies
├─ schemas/ request and response models
├─ handlers/ the functions that answer
└─ routers/ which handler answers which path
✓ dependencies point one way
The application lifecycle
01 · start
$ go run ./cmd
[Server] Starting 2 components in parallel
✓ Listening on :8080
02 · answer
$ curl localhost:8080/users/rick
{"username":"rick"}
200 · the handler's return type, encoded
03 · refuse
$ curl 'localhost:8080/users/?limit=nope'
422 "limit" must be a valid integer
every offending field, reported at once
04 · describe
$ curl -s localhost:8080/openapi.json | head -3
{ "openapi": "3.1.0",
"info": { "title": "Awesome API"
05 · drain
$ ^C
[Server] Shutting down, waiting for in-flight requests...
✓ Stopped (drained, then released)
› safe by default

Timeouts are non-zero, bodies are capped, unknown JSON members are rejected, and CORS denies everything until a policy is written. Relaxing any of it is a decision you make out loud.

The contract

Types in, types out

Struct tags say where each field is read from. The return type is the response model, with no wrapper and no filtering pass at run time, so a field you did not declare cannot leak. The same declarations produce the OpenAPI document.

routers/users.go
go
1type Params struct {
2    Username string `path:"username" doc:"The user to look up"`
3}
4
5type UserOut struct {
6    Username string `json:"username"`
7    Email    string `json:"email,omitzero"`
8}
9
10r.Get("/users/{username}", func(ctx *muzak.Context, in Params) (UserOut, error) {
11    return UserOut{Username: in.Username}, nil
12})  // type arguments inferred, never written
DERIVED CONTRACT
openapi · 3.1
usernamein: pathrequired
200UserOutapplication/json
422ErrorResponsevalidation_error
operationIdget_users_by_username
› reflected once at build time · never on the request path
and the rules cannot drift from the documentgo
v.String(&in.Email).Trim().Lower().Required().Email()
v.Slice(&in.Tags).MaxItems(10).Unique().Each(validate.String().MaxLen(20))
// → "format": "email" · "maxItems": 10 · "uniqueItems": true · items.maxLength: 20
// &in.Email is the field, so renaming it is a change the compiler checks

Benchmarks

Compiled once, not per request

Apple M1 Pro, darwin/arm64, Go 1.27, medians of five runs. Every case drives the real code path through App.ServeHTTP, so routing, binding, dependencies and encoding are all included.

Route match
0ns
static, 17-route tree
Allocations
0
per route lookup
Full request
0ns
vs 628 for ServeMux
Dependencies
0
third party, anywhere
OPERATION COSTlower is better
ns / op
lookup, static75.3lookup, param92.4route, bare786.0http.ServeMux628.4bind path+query1661bind JSON body340603406 ns
MATERIAL DATASHEET
rev 0.3.0
Param lookup92.4 ns
Validation, 8 rule sets1.4 µs
WebSocket masking13.3 GB/s
Statement coverage99.1%
Runtimenet/http · Go 1.27

FAQ

Questions, answered plainly

The short version of what Muzak is, how a route is written, and what it costs you at runtime.

What is Muzak?
Muzak is a type-safe web framework for Go, built on net/http and Go 1.27 with no third-party dependencies at all. A handler is an ordinary typed function: its input type is the request and its return type is the response body, and both are checked when you compile rather than when a request arrives.
How is a route registered?
With a generic method on a router: r.Get("/users/{username}", handler). Go 1.27 added generic methods and generalized function type inference, so the router reads the input and output types off the handler literal and you never write the type arguments yourself.
How does Muzak know where request data comes from?
From struct tags on the input type. A field tagged path, query, header, cookie, form or file is read from that part of the request; a field with no location tag is a member of the JSON body. The binding plan is compiled once per route at start-up, so the per-request path walks a list of precompiled setters and never inspects a type.
How does validation work?
An input model implements Validate(v *muzak.Validation) and declares rules against the field itself, as in v.String(&in.Email).Trim().Lower().Required().Email(). There is no tag string to typo and no field name written as text, so renaming a field is a change the compiler checks. The same declarations produce the constraints in the generated OpenAPI document.
Does Muzak generate API documentation?
Yes. An OpenAPI 3.1 document is served at /openapi.json and a self-contained reference at /docs, both derived from the routes, the input and output types and the validation rules. The page is embedded in the binary and fetches nothing from a third party, and it carries a console: fill in an operation, send it, and read the status, headers and body without leaving the page, or copy the same request as curl. Both are announced with their URLs when the server starts.
Does it support WebSockets and server-sent events?
Both, with the protocols implemented in the framework rather than delegated. A WebSocket handshake is an ordinary GET, so middleware, guards, dependencies and input binding all run before a single byte is upgraded. An event stream is typed, so the compiler enforces what a route may send and the generated document describes it.
How fast is it?
On an Apple M1 Pro, matching a static route takes about 75 nanoseconds and allocates nothing, and a full request through the framework runs in about 790 nanoseconds, against about 630 for a bare net/http ServeMux handler doing the same work by hand. Almost all of the difference is one copy of the request that carries the matched route to logging, tracing and metrics. Numbers differ by machine; the benchmarks are reproducible with go test -bench.
What are the defaults?
Conservative ones. Every listener timeout is non-zero, request bodies are capped at one mebibyte, unknown JSON members are rejected, CORS denies every cross-origin request until a policy is written, a cross-origin WebSocket handshake is refused, no forwarding header is believed until a proxy is named, and a panic becomes a generic 500 with the stack recorded only in the log. Each can be relaxed deliberately; none is relaxed by omission.
What does Muzak require?
Go 1.27. The framework uses generic methods, generalized function type inference, encoding/json/v2 and the standard library uuid package, none of which exist in earlier versions. It has no third-party dependencies.

Stop finding contract bugs in production.
Start finding them atcompile time.

$ go get muzak.dev/framework▍
Muzak logomuzak·v0.3.1
Open source under MIT / Apache-2.0.