First Steps
Muzak needs 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.
go version
Install
Create a module and add the dependency.
mkdir awesome-api && cd awesome-api
go mod init awesome-api
go get muzak.dev/framework
The smallest application
package main
import (
"log"
"muzak.dev/framework"
)
type HelloOut struct {
Message string `json:"message"`
}
func main() {
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
Version: "1.0.0",
Addr: ":8080",
})
app.Get("/", func(ctx *muzak.Context, _ muzak.Empty) (HelloOut, error) {
return HelloOut{Message: "Hello from Muzak"}, nil
})
log.Fatal(app.RunSignals())
}
Run it:
go run ./cmd
14:32:07.482 INFO [Server] Starting Muzak application...
14:32:07.492 INFO [Server] Listening on :8080
And call it:
curl http://localhost:8080/
{"message":"Hello from Muzak"}
What each piece does
muzak.Newbuilds the application.AppOptionsembedsOpenAPIOptionsandServerOptions, which is whyTitleandAddrsit next to each other in one literal.app.Getregisters a route.Appembeds*Router, so the root application has the same registration methods any nested router has.muzak.Emptyis the input type for a route that reads nothing from the request. Binding is skipped entirely for it.HelloOutis the response model. What the handler returns is what the client receives.app.RunSignalsbuilds the application, starts the lifecycle components, listens, and shuts down gracefully onSIGINTorSIGTERM.
The generated documentation
Where it is, is the last thing the server says as it starts:
INFO [Server] Listening on [::]:8080 scheme=http
INFO [Docs] OpenAPI document at http://localhost:8080/openapi.json ui="set AppOptions.DocsUI to serve a documentation page"
Every application publishes that document. It is generated from the routes and the types, and it is all a client generator, a linter or another service needs:
curl http://localhost:8080/openapi.json
Adding the dashboard
Rendering the document is a separate module, so a service that wants no page carries
none. Add the import and the option, and /docs starts serving one:
go get muzak.dev/openapi
import (
"muzak.dev/framework"
"muzak.dev/openapi/ui"
)
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
DocsUI: ui.Files(),
})
Restart, and the log names the page instead:
INFO [Docs] Documentation at http://localhost:8080/docs openapi=http://localhost:8080/openapi.json
Open it. The page is served from the binary itself: it fetches nothing from a third
party and runs under a content security policy that hashes its own inline script.
Operations are grouped by tag, every schema is an outline you can expand, and each
operation has a console that sends the request and shows you what came back -- or hands
you the same request as a curl command.
Nothing about this happens at run time. The dashboard is embedded in the binary that imports it, so an air-gapped deployment gets a working page with no further setup, and one that never imports it downloads none of those bytes.
Where they are served
Both paths are configurable, and both can be turned off entirely:
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
DocsUI: ui.Files(),
DocsPath: "/reference", // defaults to "/docs"
OpenAPIPath: "/spec.json", // defaults to "/openapi.json"
})
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
DisableDocs: true, // serve neither the page nor the document
})
Leaving DocsUI unset serves no page while keeping the document; DisableDocs stops
both. With no page configured, DocsPath is not reserved either, so a route of your own
may answer /docs.
Reading the request
A handler asks for what it needs by declaring an input type. Each field says where it comes from.
type SearchIn struct {
Query string `query:"q" doc:"What to search for"`
Limit int `query:"limit" default:"20" doc:"How many results to return"`
Page *int `query:"page" doc:"Which page to return, if any"`
}
type SearchOut struct {
Query string `json:"query"`
Limit int `json:"limit"`
Results []string `json:"results"`
}
func main() {
app := muzak.New(muzak.AppOptions{Title: "Awesome API"})
app.Get("/search", func(ctx *muzak.Context, in SearchIn) (SearchOut, error) {
return SearchOut{Query: in.Query, Limit: in.Limit, Results: nil}, nil
})
log.Fatal(app.RunSignals())
}
curl 'http://localhost:8080/search?q=muzak&limit=nope'
{
"error": {
"code": "validation_error",
"message": "The request could not be validated.",
"status": 422,
"details": [
{ "field": "limit", "location": "query", "issue": "must be a valid integer" }
]
},
"request_id": "0611f4b2-2f0a-4b57-9c1a-6e6a2e2f9b31"
}
Every offending field is reported at once, and the request_id in the body is the same
identifier the server wrote to its log and returned in the X-Request-Id header.
Project layout
One file is fine for a demonstration. A real service outgrows it quickly, so the layout
below is the one the framework's own example uses. cmd/main.go does nothing but
compose, and every piece of behaviour lives in a package of its own.
awesome-api/
├── cmd/
│ └── main.go composition, and nothing else
├── core/ configuration, guards, dependencies, managed resources
├── schemas/ the request and response models the API exposes
├── handlers/ the functions that answer requests
├── routers/ which handler answers which path
├── .env
└── go.mod
The dependencies point one way. Routers know handlers, handlers know schemas and core, and core knows nothing about any of them, so every package is testable on its own.
Here is the same hello world, written that way.
package main
import (
"log"
"awesome-api/routers"
"muzak.dev/framework"
)
func main() {
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
Version: "1.0.0",
Addr: ":8080",
})
app.Include(routers.Users())
log.Fatal(app.RunSignals())
}
Catching mistakes before the socket opens
Route problems are collected while the application is built, not while it is serving. A duplicate route, an input type that cannot be bound, a path parameter no field reads, a duplicate operation identifier and a malformed prefix are all reported together.
if err := app.Build(); err != nil {
log.Fatal(err)
}
Build is called for you by Run, RunContext, RunSignals and ServeHTTP, so
calling it directly is only useful to surface configuration errors early, which is what a
start-up check or a test wants. Building is idempotent: the work happens once, and later
calls return the same result.
Where to go next
Routers covers paths, methods and how routers compose. Request Data covers every place an input field can be read from.