Quick start

From an empty directory to a project that builds. There are four commands, and two of them are the interesting ones: check the spec, then generate from it.

1. Write a spec #

Make a directory and a .api file. A spec has three parts: a group of settings, the types the routes trade in, and the service block that names the routes.

user.api
syntax = "v1"

type (
  LoginReq {
    Username string `json:"username"`
    Password string `json:"password"`
  }
  LoginResp {
    Token string `json:"token"`
  }
  Profile {
    Id   int64  `path:"id"`
    Name string `json:"name"`
  }
)

@server (
  prefix: /api/v1
  group:  user
)

service user-api {
  @doc "log a user in"
  @handler Login
  post /login (LoginReq) returns (LoginResp)

  @handler GetProfile
  get /profile/:id returns (Profile)
}

The @server block applies to the service under it: every route in user-api is served under /api/v1 and belongs to the user group. Each route names its handler and, in parentheses, the type it takes and returns. A get route with no request type simply leaves the parentheses out.

2. Check it #

The check reads the file the same way generation will, and stops on anything it cannot account for:

terminal
$ mctl api validate user.api
mctl: user.api is valid

Forget a @handler and it says so, with the line number:

terminal
$ mctl api validate broken.api
error [syntax] line 16: route get /orders/:id has no handler; name one with `@handler`
mctl: broken.api: 1 error(s), 0 warning(s)

mctl api format rewrites the file in the one layout the tool considers canonical — comments and struct tags untouched, indentation and blank lines fixed. Pass --write to do it in place, or leave it off to get the text on stdout:

terminal
$ mctl api format user.api --write
mctl: formatted user.api

3. Generate #

One command writes the whole project into the current directory:

terminal
$ mctl gen api user.api
mctl: wrote moon.mod
mctl: wrote moon.pkg
mctl: wrote user-api.mbt
mctl: wrote etc/user-api.yaml
mctl: wrote internal/config/moon.pkg
mctl: wrote internal/config/config.mbt
mctl: wrote internal/svc/moon.pkg
mctl: wrote internal/svc/servicecontext.mbt
mctl: wrote internal/types/moon.pkg
mctl: wrote internal/types/types.mbt
mctl: wrote internal/handler/moon.pkg
mctl: wrote internal/handler/routes_test.mbt
mctl: wrote internal/handler/routes.mbt
mctl: wrote internal/handler/user/moon.pkg
mctl: wrote internal/handler/user/loginhandler.mbt
mctl: wrote internal/handler/user/getprofilehandler.mbt
mctl: wrote internal/logic/user/moon.pkg
mctl: wrote internal/logic/user/loginlogic.mbt
mctl: wrote internal/logic/user/getprofilelogic.mbt
mctl: wrote 19 file(s) from user.api
Where it writes

Into the current directory by default, under --dir out when you name one. --flat skips the tree and writes the routes and types as a single .mbt file.

What was written #

Some of these files belong to the spec and are rewritten every time; the rest are written once and then belong to you.

FileOn the next runWhat it holds
moon.mod, moon.pkgoncethe module and the root package
user-api.mbtoncethe entry point: read the config, build the context, serve HTTP
etc/user-api.yamloncehost and port
internal/config/config.mbtoncethe configuration record
internal/svc/servicecontext.mbtonceeverything the handlers share
internal/types/types.mbtevery runthe spec's types
internal/handler/routes.mbtevery runthe route table, prefix applied
internal/handler/moon.pkgevery runthe imports the handler tree needs
internal/handler/user/*handler.mbtoncethe HTTP edge of one route
internal/logic/user/*logic.mbtoncewhere the work goes
internal/handler/routes_test.mbtoncea test that calls every route

Run the same command again and it says which of the two it did:

terminal
$ mctl gen api user.api
mctl: wrote internal/types/types.mbt
mctl: wrote internal/handler/moon.pkg
mctl: wrote internal/handler/routes.mbt
mctl: wrote 3 file(s) from user.api; kept the 16 already there

Three files, not nineteen. A comment you add to a logic file is still there after the run — regeneration rewrites what the spec decides and reads everything else.

4. Fill in the logic #

Each route got a handler and a logic file. The logic file is the one to open:

internal/logic/user/loginlogic.mbt
///|
/// `post /api/v1/login` — log a user in. This is where the service does its work.
pub fn loginlogic(
  _svc : @svc.ServiceContext,
  _ctx : @moonapi.Context,
) -> @moonasgi.Response {
  @moonapi.text(200, "TODO: Login")
}

Replace the body. The signature stays as it is — the handler above it is generated and calls this function, so the two agree on the shape without either being edited.

5. Build it #

The project that came out compiles as it stands, for all four targets:

terminal
$ moon check --target all
Finished. moon: ran 339 tasks, now up to date

moon run . starts the service on the host and port in etc/user-api.yaml — 0.0.0.0:12000 until you change it — and the entry point prints the address it is serving on. Building a native binary links through a C toolchain, so that step needs one installed; moon check does not.

Where next #

The spec language has more in it than one example shows: tags that become validation, nested and embedded types, several groups in one service. The clients page takes the same spec and generates the other side of the wire.