Skip to content

Keywords ​

The DSL has 16 keywords plus the seven HTTP verbs. They are reserved - identifiers cannot use these names, with the contextual carve-outs noted below.

Declaration keywords ​

KeywordPositionPurpose
packagefirst lineThe package every declaration in this file belongs to
typetop levelDeclare a request / response struct
enumtop levelDeclare a closed value set
errortop levelDeclare a typed error with HTTP status mapping
scalartop levelDeclare a named primitive with bundled validators
servicetop levelDeclare an HTTP service
extendtop levelAdd methods to an existing service (extend service Name)
middlewaretop levelDeclare a named middleware slot
eventtop levelDeclare an event contract this package owns

A service body holds HTTP methods only. event is a file-level declaration and there is no consumer declaration at all - which events a deployable listens to is Go code, written where its bus is built. See Events.

Member body keywords ​

KeywordWherePurpose
requestinside method bodyNames the request type
responseinside method bodyNames the response type
payloadinside event bodyNames the type an event contract carries (or an array of it: Type[])

Type keywords ​

KeywordWherePurpose
maptype expressionMap type: map<string, int>

Literal keywords ​

KeywordPurpose
trueBoolean true literal
falseBoolean false literal
nullNull literal (used in some decorator args)

Contextual use ​

A reserved word is still legal wherever the grammar leaves no ambiguity: as a field name in a type body, as an enum value name, as a decorator argument naming a field, and as a path segment or path-parameter name. type Msg { event string? payload string? } with @requiresOneOf(payload, event) above it is a valid design.

HTTP verbs ​

These behave like keywords inside a service body. They are also legal as identifiers in other positions (e.g. as an enum value name), so the parser disambiguates by context.

VerbMaps to
getGET
postPOST
putPUT
patchPATCH
deleteDELETE
headHEAD
optionsOPTIONS

package ​

The first statement of every .craftgo file that declares anything - only comments and the file-level decorators @doc, @deprecated and @version may come before it. A file without one is rejected as package/missing at its first declaration:

craftgo
package design

Files that declare the same package name form one package, whatever folder they sit in: declarations in design/users/service.craftgo and design/users/errors.craftgo see each other directly because both declare package design. The folder plays no part - two files in one folder that declare different names are two packages.

The package name does not need to match the folder name (though doing so reads cleaner).

Cross-package references ​

Reach a declaration from a different package by qualifying it with that package's name - no import statement is needed:

craftgo
package design

type User {
    contact shared.Contact
}

craftgo resolves <pkg>.<Type> against every package in the project (the package X declaration is the name) and wires the matching Go import in the generated code automatically. Middleware names are global across the project; type / enum / error / scalar names live in their declaring package and must be qualified at the call site (shared.Audit, users.User).

Note: there is no import statement. A line starting with import at file level is a parse error that says to reference the declaration as pkg.Type and delete the line. import is not reserved, so a field may take it as its name.

type ​

Declare a request or response struct.

craftgo
type CreateUserReq {
    name  string
    email string @format(email)
}

type Page<T> {                  // generic - bare ident, no constraint
    items T[]
    total int
}

See Types and Scalars.

enum ​

Declare a closed value set with three forms (bare / integer / string):

craftgo
enum Status { Active  Inactive  Pending }
enum Priority { Low = 1  Medium = 2  High = 3 }
enum Color { Red = "red"  Green = "green"  Blue = "blue" }

All values inside one enum must share the same form. Mixing them (e.g. Active = 1 Inactive = "off") raises enum/mixed-types. Enums are not generic - only type declarations support <T, U, ...> parameters.

See Enums.

scalar ​

Declare a named primitive with bundled validators:

craftgo
scalar Email     string  @format(email) @maxLength(254)
scalar OrderID   string  @length(8, 64) @pattern("^ord_[A-Z0-9]+$")
scalar Cents     int     @gte(0) @multipleOf(2)
scalar Latitude  float64 @gte(-90) @lte(90)

The first form after the name is the underlying primitive (string, bytes, int, int8/16/32/64, uint, uint8/16/32/64, float32, float64, bool). Validators following the primitive inherit to every field of that scalar type.

See Types and Scalars.

error ​

Declare a typed error. Two forms:

craftgo
error NotFound UserNotFound                    // empty body, 404

error Conflict EmailTaken {                    // body fields, 409
    email      string
    existingId string?
}

The first identifier after error is the HTTP category (one of 21 reserved names like BadRequest, NotFound, Conflict, Internal). The second is the error's name; its Go type is that name plus Err (UserNotFoundErr) unless it already ends in Err or Error, and a body block becomes the embedded <Name>Body struct. The optional body block carries fields that ride on the wire.

See Errors.

service ​

Declare an HTTP service:

craftgo
@prefix("/v1")
@tags(users)
@middlewares(AuthRequired)
service UserService {
    @doc("Get user by id.")
    get GetUser /users/{id} {
        request  GetUserReq
        response User
    }
}

The body holds zero or more method declarations. Method form: <verb> <Name> [<path>] { request <Type> response <Type> }. The request and response lines are optional (a method may have neither, only request, or only response), and so is the path: without one, the route is the method name in kebab case.

See DSL Basics for path parameters, decorators, and the full method shape.

extend service ​

Add methods to an existing service from a different file:

craftgo
package design

extend service UserService {
    @middlewares(AdminOnly)
    delete PurgeUser /users/{id}/purge {
        request  GetUserReq
        response shared.OkResp
    }
}

extend blocks may carry method-level-applicable decorators - any decorator a method takes, such as @middlewares, @security, @tags or @timeout - which propagate to every method inside, plus @group, which moves that block's methods into the group's directory. @prefix is primary-only, and @operationId belongs on each method. The extended service must already exist in the same package.

Used to split a large service across files, separate authenticated endpoints from public ones (the 50/50 pattern: primary holds public methods, an extend block holds the authenticated chain), or organise admin endpoints under a different middleware chain than the default. See DSL Basics for the full pattern.

middleware ​

Declare a named middleware slot:

craftgo
middleware AuthRequired
middleware RateLimit
middleware CORS

Declared at file (package) level. Codegen produces a typed slot on ServiceContext and an empty stub at internal/middleware/<name>_middleware.go you fill in. Methods opt in via @middlewares(Name, ...). Middleware names are global across the project - @middlewares(AuthRequired) resolves the same regardless of package - because middleware represents runtime behavior, not data; type / enum / error / scalar names stay package-scoped and must be qualified across packages.

See Middleware.

event ​

Declare an event contract this package owns:

craftgo
@doc("Emitted once an order is accepted.")
@contract("order.placed.v2")
event Placed {
    payload OrderPlaced
}

File level only - a service body holds HTTP methods. payload names a type declaration, so every contract has named fields and its own Validate(); payload OrderPlaced[] is a contract carrying a JSON array of that type. Events have their own namespace, so type OrderPlaced and event OrderPlaced coexist. The wire identity defaults to <package>.<Event> and @contract("...") overrides it. Codegen writes one constant and one descriptor per event; the listener side is ordinary Go against the bus.

See Events.

request and response ​

Used inside a method body to name the request and response types:

craftgo
post CreateUser /users {
    request  CreateUserReq
    response User
}

Both are optional. Methods without request accept no body. Methods without response return an empty body with the configured status.

Each clause names a type. A bare array (Order[]) and a built-in primitive (string, bytes, any, ...) are rejected in both clauses - neither has the fields a request binds, and a primitive names no generated type at all - so wrap the value instead: type Resp { value string }. A scalar or an enum is rejected in request for that same fieldless reason, but is fine in response, where it generates a real named type.

map ​

Used inside a type expression for map types:

craftgo
type Settings {
    flags  map<string, bool>
    quotas map<string, int>
}

The first generic argument is the key type and the second is the value type (any DSL type). The key must be a string- or integer-kind primitive (string, int*, uint*) or a scalar/enum over one - encoding/json can only marshal those as object keys. A bool, float*, or struct key is rejected at design time (it would compile but fail json.Marshal at runtime).

Reserved names you cannot use as identifiers ​

A reserved word cannot name a declaration: type event { ... } is a parse error (expected Ident, got event). In the positions under Contextual use it is fine: a field named type becomes the Go field Type with JSON key type.

File grammar in one shape ​

[@doc|@deprecated|@version]*
package <ident>

[<decl>]*

where <decl> is one of:
  [@decorator]* type Name { fields... }
  [@decorator]* type Name<T, U, ...> { fields... }    // upper-case parameter names, no constraint
  [@decorator]* enum Name { values... }
  [@decorator]* error Category Name [{ fields... }]
  [@decorator]* scalar Name <Primitive> [@validators...]
  [@decorator]* service Name { methods... }
  [@decorator]* extend service Name { methods... }
  [@decorator]* middleware Name
  [@decorator]* event Name { payload Type }        // or `payload Type[]` for an array body

and a method is:
  [@decorator]* <verb> Name [/path] { [request Type] [response Type] }

Released under the MIT License.