This is the house style for writing idiomatic Marreta. Following it keeps routes readable
and makes marreta fmt and marreta lint predictable across a project.
Indentation
Indentation is significant. It defines blocks: route and task bodies, match arms,
transaction blocks, and any nested structure. The structure of the code comes from how it
is indented, so consistency is not optional.
route POST "/orders" take payload as NewOrder
require payload.total else fail 400, "total is required"
transaction
order = db.orders.save({ total: payload.total })
db.line_items.save({ order_id: order.id })
reply 201, { id: order.id }- Indent with spaces, 4 per level. The language only requires consistent widths, but every project uses 4, so do the same.
- Pressing Tab is fine when your editor inserts spaces, but a literal tab character is rejected. If you hit an indentation error, set your editor to insert spaces.
- A dedent must return to a level you opened before, otherwise it is an error.
- Blank lines and comment-only lines do not affect indentation.
Naming
| Construct | Convention | Example |
|---|---|---|
| Variables | snake_case | user_id, order_total |
| Tasks | snake_case | task calculate_tax(item) |
| Task parameters | snake_case | task apply_discount(base_price) |
| Schema names | PascalCase | UserPayload, NewOrder |
| Schema fields | snake_case | first_name, is_active |
| Optional schema fields | snake_case? | email?, phone_number? |
| Auth providers | snake_case | internal_auth, entra_id |
Globals (app.marreta) | snake_case | project_name = "payments-api" |
It is snake_case everywhere with one exception. Variables, tasks, parameters, and fields
are snake_case. Schema names are the exception: they name a type, so they use PascalCase.
Schemas
schema UserPayload
name: string
age: integer
email?: string
is_active: boolean- Schema names use
PascalCase, the one place that is notsnake_case. - Fields are
snake_case, one per line, indented 4 spaces. - Optional fields use
?on the field name, not the type.
Routes
route POST "/users" take payload as UserPayload
require payload.name else fail 400, "name is required"
reply 201, { id: 1 }- Route paths use lowercase and hyphens:
"/user-profiles","/order-items/:id". - Bind schemas with
as SchemaNameimmediately after the take binding.
Tasks
task calculate_discount(price, category)
rate = match category
"vip" -> 0.15
"premium" -> 0.10
fallback -> 0.0
price * rate- Name a task as a verb in its base form, since a task does something:
calculate_tax,load_profile, nottax,calculated_tax, orloading_profile. - Prefer inline tasks (
=>) for a single expression, and block tasks for multi-step logic. - There is no explicit
return. The last expression is the implicit return.
Guards
require payload.user_id else fail 400, "user_id is required"
reject client.blocked else fail 403, "account blocked"- Use
requirefor “must be truthy” checks andrejectfor “must be falsy” checks. - Place all guards at the top of the route body, before business logic.
Responses
reply 200, { users: users, total: total }
reply 201, { id: new_user.id }
reply html 200, "<h1>Welcome</h1>"
reply text 200, "pong"
fail 404, "user not found"- Prefer
failfor early error exits.replycan intentionally model any HTTP status, including 4xx and 5xx, for example when mirroring an upstream response. replyandfailterminate execution immediately, so no code after them runs.
Auth
auth api_key internal_auth {
header: "x-api-key"
secret_hash: env.INTERNAL_KEY_HASH
principal: "service"
}
route GET "/reports"
require auth internal_auth
allow auth.user.id == "service"
reply 200, { ok: true }- Provider names are
snake_case, like variables. The provider type (api_key,jwt) comes first, then the name. - Keep secrets out of the source, reading them from
envinmarreta.env. - Place
require authandallowat the top of the route, with the other guards. - Role checks like
allow "reports.read" in auth.user.rolesneed a provider whose tokens carry roles, likejwt. Anapi_keyprincipal carries no roles, so authorize it onauth.user.id.
Comments
# Route: list all active users
route GET "/users"
limit = query.limit or 10 # default page size
reply 200, users- Use
#for all comments, with one space after the#(marreta fmtadds it, turning#noteinto# note). Divider styles like## sectionand a bare#are left as they are. - Prefer comments that explain why, not what.
Formatting
marreta fmt rewrites a project to this house style so the layout is never a review topic.
It formats every .marreta file the project loads (the same files marreta serve and
marreta test read, including any in non-standard folders like auth/), and it enforces:
- four-space indentation (see Indentation above),
- one space around operators and after commas and colons (intra-line spacing),
- runs of blank lines collapsed to at most one,
- exactly one newline at the end of the file, and no blank lines at the file’s start or end,
- one space after the leading
#of a comment (see Comments above).
These are decisions, not omissions, and the formatter deliberately stops there:
- No line wrapping and no maximum line width. In Marreta a newline and an indent are meaning-bearing (they separate statements and open blocks), so wrapping a line would change the program’s structure, which is exactly what the formatter is built never to do. Line length is the author’s call.
- No alignment. Fields and assignments are not padded into columns, because editing one line would then reflow its neighbors and add noise to every diff.
- No sorting. The order of fields, routes, and declarations is meaningful (it is the document order of the generated OpenAPI and the author’s reading order), so the formatter keeps it.
- No comment reflow. The text of a comment belongs to the author. The formatter only
fixes the single space after the leading
#, never the words.
File structure
project/
├── marreta.env # infrastructure config (never commit secrets)
├── app.marreta # entry point, global metadata only
├── routes/
│ ├── users.marreta
│ └── orders.marreta
├── schemas/
│ └── payloads.marreta
├── tasks/
│ └── calculations.marreta
└── tests/
└── users_test.marreta- One concern per file. Route files in
routes/, shared tasks intasks/, shared schemas inschemas/. - Scenario tests live in
tests/with the_test.marretasuffix, which is howmarreta testandmarreta doctordiscover them. A test outsidetests/or without the suffix is not picked up. marreta.envis for config only, no logic. Keepapp.marretametadata-only: the language allows routes and tasks there, but prefer them in their own directories.
Multi-file and scoping
All symbols are file-private by default. Use export to share a task or schema across files,
reaching it through its file namespace as file.task:
# tasks/calculations.marreta
export task calculate_discount(price) => price * 0.9
internal_rate = 0.05 # private, not visible outside this file- Never use
exportin route files, since routes are never shared. exporta task or schema only when it is used in more than one file.- Name conflicts on
exportare a load error, so use distinct names. - In
app.marretaeverything is implicitly global, so noexportis needed.