Regeneration is the normal case
Files the spec decides — routes, types, wire formats — are rewritten on every run. Handler bodies, logic and tests are written once and never touched again.
moonctl reads a .api spec, a .proto file or a live database
schema and writes the MoonBit around it: routes, types, handlers, typed clients in
four languages, models and migrations. Change the spec and run it again — the files
it owns are refreshed, the ones you wrote are left alone.
The same parse feeds every generator, so the route your server answers and the method your client calls cannot drift apart.
mctl api ts
// Code generated by `mctl api ts` from a `.api` spec. DO NOT EDIT.
export class UserApiClient {
readonly baseUrl: string;
token?: string;
constructor(baseUrl: string, fetchImpl?: Fetch, token?: string) {
this.baseUrl = baseUrl.replace(/\/+$/, "");
this.fetchImpl = fetchImpl ?? ((url, init) => fetch(url, init));
this.token = token;
}
async login(body: LoginReq): Promise<LoginResp> {
const path = `/api/v1/login`;
const url = this.baseUrl + path;
const headers: Record<string, string> = { accept: "application/json" };
if (this.token !== undefined) {
headers["authorization"] = "Bearer " + this.token;
}
const res = await this.fetchImpl(url, {
method: "POST",
headers,
body: JSON.stringify(loginReqToJson(body)),
});
if (!res.ok) {
throw new ApiError(res.status, await res.text());
}
return loginRespFromJson(JSON.parse(await res.text()));
}
}
mctl api dart
// Code generated by `mctl api dart` from a `.api` spec. DO NOT EDIT.
import 'dart:convert';
class UserApiClient {
UserApiClient({required String baseUrl, required this.send, this.token})
: baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), '');
final String baseUrl;
final Send send;
final String? token;
Future<LoginResp> login(LoginReq body) async {
final path = '/api/v1/login';
final url = Uri.parse('$baseUrl$path');
final headers = <String, String>{'accept': 'application/json'};
if (token != null) {
headers['authorization'] = 'Bearer $token';
}
final res = await send('POST', url, headers, jsonEncode(body.toJson()));
if (!res.ok) {
throw ApiException(res.status, res.body);
}
return LoginResp.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
}
}
mctl api kt
// Code generated by `mctl api kt` from a `.api` spec. DO NOT EDIT.
class UserApiClient(
private val baseUrl: String,
private val transport: Transport,
private val codec: Codec,
private var token: String? = null,
) {
fun login(body: LoginReq): LoginResp {
val path = "/api/v1/login"
val url = baseUrl + path
val headers = linkedMapOf("accept" to "application/json")
token?.let { headers["authorization"] = "Bearer " + it }
val res = transport.send("POST", url, headers, codec.encode(body))
if (!res.ok) {
throw ApiException(res.status, res.body)
}
return LoginResp.fromWire(codec.decode(res.body))
}
fun getProfile(id: String): Profile {
val path = "/api/v1/profile/" + enc(id)
val res = transport.send("GET", baseUrl + path, linkedMapOf("accept" to "application/json"), null)
if (!res.ok) {
throw ApiException(res.status, res.body)
}
return Profile.fromWire(codec.decode(res.body))
}
}
mctl api java
// Code generated by `mctl api java` from a `.api` spec. DO NOT EDIT.
public final class UserApiClient {
public Profile getProfile(String id) {
String path = "/api/v1/profile/" + enc(id);
String url = baseUrl + path;
Map<String, String> headers = new LinkedHashMap<>();
headers.put("accept", "application/json");
if (token != null) {
headers.put("authorization", "Bearer " + token);
}
RawResponse res = transport.send("GET", url, headers, null);
if (!res.ok()) {
throw new ApiException(res.status, res.body);
}
return Profile.fromWire(codec.decode(res.body));
}
}
mctl api doc
# user-api A MoonAPI service with 2 route(s) and 3 type(s). ## Routes | Method | Path | Handler | Request | Response | Description | | --- | --- | --- | --- | --- | --- | | POST | `/api/v1/login` | `Login` | `LoginReq` | `LoginResp` | log a user in | | GET | `/api/v1/profile/:id` | `GetProfile` | — | `Profile` | — | ### Groups | Group | Prefix | JWT | Middleware | Max body | Timeout | | --- | --- | --- | --- | --- | --- | | `user` | `/api/v1` | — | — | — | — | ### Profile | Field | Type | In | Required | Constraints | | --- | --- | --- | --- | --- | | `id` | `Int64` | path | yes | | | `name` | `String` | body | yes | |
mctl migrate
-- One table per `type` block of the spec. A spec says what a value looks
-- like, not what the database should enforce: indexes, uniqueness and foreign
-- keys are yours to add.
CREATE TABLE IF NOT EXISTS `profiles` (
`id` BIGINT NOT NULL,
`name` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `login_reqs` (
`username` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL
);
Write the spec, generate, then go and write the part only you can write.
Files the spec decides — routes, types, wire formats — are rewritten on every run. Handler bodies, logic and tests are written once and never touched again.
Clients, models, docs and migrations come from the same spec tree. Rename a route and every output is renamed in the same run.
The library is pure logic. Generated clients carry a transport port — fetch, a function, a class — instead of a runtime dependency.
A line the parser cannot read is an error with a line number, never a silently smaller spec. mctl api validate lists what is wrong before any code is written.
A spec, a check, a project.
syntax = "v1"
type (
LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
LoginResp {
Token string `json:"token"`
}
)
@server (
prefix: /api/v1
group: user
)
service user-api {
@doc "log a user in"
@handler Login
post /login (LoginReq) returns (LoginResp)
}
$ mctl api validate user.api mctl: user.api is valid $ mctl api format user.api --write mctl: formatted user.api
$ 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/config.mbt mctl: wrote internal/svc/servicecontext.mbt mctl: wrote internal/types/types.mbt mctl: wrote internal/handler/routes.mbt mctl: wrote internal/handler/user/loginhandler.mbt mctl: wrote internal/logic/user/loginlogic.mbt mctl: wrote 19 file(s) from user.api
What lands in each file is in the quick start.
A command and a file. The full surface is in the reference.
.proto, with messages and codecs.
mctl api ts · dart · kt · java
A typed client per route, in four languages, with no dependencies.
mctl api doc
A Markdown reference of the routes, groups and types.
mctl migrate
The SQL a spec's models imply, one table per type block.
mctl model mongo
Document models and typed repositories over a collection port.
mctl gateway
An HTTP front door in front of a .proto's RPCs.
mctl quickstart
Scaffold, generate and build a service in one step.
mctl api validate
Every finding, with a line number, before any code exists.
Write a service block, one type, one route. Run the check, then the generator. The project that comes out builds as it is.