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 framework also ships a muzak command, which can create the project for you, laid out
like the one below, and rebuild and restart it while you work on it:
go install muzak.dev/framework/cmd/muzak@latest
muzak new awesome-api
cd awesome-api && go mod tidy
This page builds the project by hand so that every piece is visible. See Command Line for what the command does.
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 in a sidebar by category, or by tag where a route has none, 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. See OpenAPI for how to file a
router under a category and give a route a title.
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. Guards given to muzak.New cover the documentation too; see
OpenAPI.
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.
muzak new writes a smaller project of the same shape: routers, handlers and schemas
with one route, its handler and its models, a handler test that serves the application with
testclient, and the server in cmd/server/main.go rather than cmd/main.go. It writes no
core package and no .env; add them when the service needs them. See
Command Line.
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.