Muzak logomuzak
v0.1.10

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]    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 names its own script by hash. 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.

The OpenAPI 3.1 document behind it is at http://localhost:8080/openapi.json.

curl http://localhost:8080/openapi.json

Both paths are configurable, and both can be turned off entirely:

app := muzak.New(muzak.AppOptions{
    Title:       "Awesome API",
    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
})

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.