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.New builds the application. AppOptions embeds OpenAPIOptions and ServerOptions, which is why Title and Addr sit next to each other in one literal.
  • app.Get registers a route. App embeds *Router, so the root application has the same registration methods any nested router has.
  • muzak.Empty is the input type for a route that reads nothing from the request. Binding is skipped entirely for it.
  • HelloOut is the response model. What the handler returns is what the client receives.
  • app.RunSignals builds the application, starts the lifecycle components, listens, and shuts down gracefully on SIGINT or SIGTERM.

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.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.