DSL Basics
The craftgo DSL is a small file format that describes your API. From it, craftgo generates Go types, validators, HTTP handlers, route registration, and an OpenAPI 3.1 spec.
At a glance
A .craftgo file has two parts:
- A
packageline, first in the file - only comments and the file-level decorators@doc,@deprecatedand@versionmay come before it. A file that declares anything without one is rejected aspackage/missingat its first declaration; a file holding only comments needs none. The name is also the generated Go package's, so a Go keyword, a predeclared Go identifier such asintorlen,main,initand_are rejected aspackage/name - Declarations:
type,enum,scalar,error,service(andextend service),middleware,event
Declaration names start with an upper-case letter: a lower-case type, enum, scalar, error, middleware, event or method name is rejected as decl/name-case (the Go identifier generated from it would be unexported); a lower-case service name only warns. Type parameter names follow the same rule (type Page<T>, not <t>).
Every declaration produces specific generated code. The DSL is the single source of truth: change a field once, every generated artifact updates.
package design
type CreateUserReq {
name string @length(1, 80)
email string @format(email)
}
type User {
id string
name string
email string
}
@prefix("/v1")
service UserService {
post CreateUser /users {
request CreateUserReq
response User
}
}This page covers the syntax. For per-decorator detail see Decorators. For runtime behavior see Runtime.
File layout
A craftgo project keeps its DSL files under one design folder - the folder holding craftgo.design.yaml, design/ by default. A package is every file that declares the same package name, in any subfolder; one subfolder per package is the usual layout.
design/
├── craftgo.design.yaml
├── users/
│ ├── service.craftgo
│ └── errors.craftgo
└── orders/
└── service.craftgoFiles that declare the same package see each other's declarations directly, whatever their folder. To use a declaration from another package, qualify it with that package's name (shared.Type) - cross-package references resolve automatically, no import needed.
Seven declaration kinds
Every craftgo file declares from this set:
| Keyword | Purpose |
|---|---|
type | Request / response struct |
enum | Closed value set |
scalar | Named primitive with bundled validators |
error | Typed error with HTTP status |
service | Group of HTTP methods |
middleware | Named middleware slot |
event | Event contract this package owns |
package design
type User { id string name string }
enum Status { Active Inactive }
scalar Email string @format(email)
error NotFound UserNotFound
service UserService {}
middleware Auth
event UserCreated { payload User }A service body holds HTTP methods and nothing else; event is a file-level declaration like the rest. Methods are covered below; see Events for contracts and the listeners that receive them.
Types
Describe the shape of request and response bodies.
type CreateUserReq {
name string
email string
age int?
tags string[]
meta map<string, string>
}Field syntax: name TypeRef [@decorator(...) ...]. Type references are primitives, arrays (T[]), maps (map<K, V>), or other declared types. Append ? to mark optional. A type always has a body: type T alone is a parse error.
| DSL form | Go output |
|---|---|
string | string |
int / int64 | matching Go integers |
float64 | float64 |
bool | bool |
bytes | []byte |
T? | *T (an optional array, map or bytes stays []T / map[K]V / []byte, nil when absent) |
T[] | []T |
map<K, V> | map[K]V |
Custom | Custom (your type) |
See Types and Scalars for advanced shapes (generics, mixins).
Decorators
Decorators attach metadata. They start with @ and may take arguments.
type CreateUserReq {
name string @length(1, 80)
email string @format(email)
age int? @gte(0) @lte(150)
}53 decorators, grouped by purpose: validators, bindings, metadata, service-level. Full reference at Decorators.
A decorator on a line of its own decorates what is below it: the next field, enum value, method or declaration, across blank lines and comments, as in Java, TypeScript or Python. A field, an enum value or a scalar also takes the decorators after it on its line, and those that follow them on the line where a multi-line argument list closes:
type Signup {
id string
@format(email)
@maxLength(254)
email string @minLength(3)
}Here email carries @format, @maxLength and @minLength, and id carries none. Decorators on lines of their own with nothing below them in the body are a parse error (decorator @doc has no field below it), and so is an @ whose name is not on its line. craftgo fmt prints the decorators written above a member one per line above it, and those written on its line after its type. A declaration's or method's decorators go before its keyword; one after a declaration or method on the same line is a parse error (decorator @doc follows a declaration on its line; a decorator goes before what it decorates) unless the next declaration starts on that line. A decorator cannot be another decorator's argument: @doc(@deprecated) is a parse error (a decorator cannot be an argument of @doc).
Services
A service is a group of HTTP methods sharing a path prefix and middleware chain.
@prefix("/v1")
@tags(users)
service UserService {
@doc("Fetch a user.")
get GetUser /users/{id} {
request GetUserReq
response User
}
@doc("Create a user.")
@status(201)
post CreateUser /users {
request CreateUserReq
response User
}
}Method form: <verb> <Name> [<path>] { request <Type> response <Type> }; both clauses are optional, and without a path the route is the method name in kebab case (get NoPath { ... } serves /no-path). A // in a path is a parse error (empty path segment ('//')), even where a comment was meant, as in /users// list: set the comment off by a space. A path ending in / is one too, the root path / aside.
Each method writes <name>.go holding <Name>Service and its constructor New<Name>Service into its service's directory, so the methods of one directory may not write one file (GetURL beside GetUrl) or one Go name (Order beside NewOrder), and no method is named Logger, the log.Logger every logic type embeds: service/method-name-clash. Nor may the file be one the go command sets apart: under the default snake file case RunTest writes run_test.go, built only for tests, and ListWindows writes list_windows.go, built only on Windows (service/method-file-name).
Verbs: get, post, put, patch, delete, head, options. trace and connect are not supported.
A request names a type (a generic instantiation such as Page<Order> included); a response names a type, an enum or a scalar. Bare arrays (response Order[]), optional markers (response User?) and built-in primitives (response string) are rejected in both clauses - wrap the shape in a type (type Items { items Order[] }) and reference that instead.
Path parameters use {name} and bind to the request field of that name (@path makes it explicit, @path("name") binds a field named otherwise):
type GetUserReq {
id string @path
}A path that declares {name} segments requires a request struct whose fields cover every segment; otherwise the route would parse the URL but the handler would never see the value, so the semantic phase rejects it with path/param-missing. The exception is a raw-request method (@rawRequest / @passthrough) with no request block: logic receives the raw *http.Request and reads the value with r.PathValue. A literal segment holds letters, digits, -, ., _ and ~, so /.well-known/jwks.json, /v1.0/users and /reports/2024 are routes as written; a segment of . or .. alone is route/pattern, since net/http cleans it out of a request path. A trailing slash (/users/) is a parse error: the route is built from segments, and a net/http pattern ending in / would match a whole subtree. The root route /, with no @prefix, registers with {$} (GET /{$}, or GET /api/{$} under the base path /api) for the same reason: it answers its one path, not every path below it.
Extending a service across files
Real services grow. To split methods across files (or add admin endpoints in a separate file from the public ones), use extend service:
// design/users/service.craftgo - the primary block
package design
@prefix("/users")
@middlewares(AuthRequired)
service UserService {
get GetUser /{id} {
request GetUserReq
response User
}
}// design/users/admin.craftgo - additional methods, same service
package design
extend service UserService {
@middlewares(AdminOnly)
delete PurgeUser /{id}/purge {
request GetUserReq
response shared.OkResp
}
}After codegen, both methods live under the same service, sharing the /users prefix and the AuthRequired middleware. PurgeUser additionally runs AdminOnly.
extend service carries its own decorators
The extend block itself can declare method-level-applicable decorators that propagate to every method in the block. The canonical use case is the 50/50 split: half the methods need auth, half don't.
service Users {
// Public endpoints - no service-level decorators
get Healthz /healthz { response HealthResp }
post Signup /signup { request SignupReq response User }
post Login /login { request LoginReq response Session }
}
@middlewares(AuthRequired)
@security(Bearer)
extend service Users {
get List /users { response UserList } // inherits AuthRequired + Bearer
get Get /users/{id} { request GetUserReq response User } // inherits
post Create /users { request CreateUserReq response User } // inherits
}The extend block's @middlewares / @security decorators apply to every method inside as if they were written directly on the method. Each method can still add its own decorators on top - those append.
Rules (enforced at gen time with a diagnostic, not silently):
- The primary
serviceblock declares@prefix(the URL prefix is whole-service). extend serviceblocks may carry method-level-applicable decorators (any decorator a method takes, such as@middlewares,@security,@tagsor@timeout, which every method of the block inherits) plus@group(which moves that block's own methods into the group's directory).@prefixon an extend raisesservice/extend-decorator-not-method, as does@operationId, which names a single operation.- The extended service must be declared in the same package - a file declaring the same
packagename, in any folder, before or after the extend block; an extend whose primary lives in another package raisesservice/extend-orphan. - Multiple
extendblocks for the same service are allowed (one per file is the typical pattern). Each block contributes its own decorators only to its own methods.
The extended methods inherit every service-level decorator from the primary AND every decorator on the extend block. Method-level decorators of the same kind (@middlewares, @security, @tags) append; use @ignoreMiddleware / @ignoreSecurity / @ignoreTags to drop the inherited chain for one specific method, or on the extend block to drop the primary's chain for every method of the block.
See Decorators - Service-level decorators and inheritance for the full combine semantics and combinations cheatsheet.
When to use it:
- Split a large service across files for navigability (admin vs public, read vs write)
- Group methods by feature area (
profile.craftgo,billing.craftgo,notifications.craftgo) - Separate frequently changed endpoints from stable ones
If your service has 5 methods, keep them in one file. extend shines around 10+ methods or when methods cluster by audience.
Enums
Closed value sets:
enum Status {
Active
Inactive
Pending
}Three forms: bare identifiers (Go string constants), = 1 (integer), = "active" (custom string). See Enums.
Scalars
Named primitives with built-in validators:
scalar Email string @format(email) @maxLength(254)
scalar Cents int @gte(0) @multipleOf(2)
type Order {
email Email
total Cents
}Every field of type Email automatically runs @format(email) and @maxLength(254). See Types and Scalars.
Errors
Typed errors with HTTP status mapping:
error NotFound UserNotFound
error Conflict EmailTaken {
email string
}See Errors.
Middleware
Declared at file level, attached to services or methods via @middlewares:
middleware AuthRequired
middleware RateLimit
@middlewares(AuthRequired, RateLimit)
service UserService {}See Middleware.
Cross-package references
Reference a declaration from another package by qualifying it with that package's name - no import line needed (the same package in another folder needs no qualifier):
package design
type User {
contact shared.Contact
}The codegen wires the matching Go imports automatically.
Each package's types are one Go package, so two packages cannot reference each other's types in a cycle: design using shared.Contact while shared uses a design type is rejected as ref/package-cycle. An event payload does not count - events are generated outside the types packages.
Comments
// line comments. Comments above a declaration become its doc string: the OpenAPI description, the Go doc of the generated declaration and the editor hover:
// User is the public user entity.
// Email is the canonical login id.
type User {}Below the decorators of a declaration, a field or an enum value, the comment right above its keyword or name joins the doc too, after the one above the decorators:
// Order is a placed order.
@deprecated
// Use Purchase instead.
type Order {}A blank line between that comment and the keyword leaves it out of the doc. A comment may also sit among the arguments of a decorator or a generic type, or between the words of a declaration's header; craftgo fmt keeps it there, an argument list it sits in one argument per line. // only - no /* */. A line ends at \n, \r\n or a lone \r, and a UTF-8 byte-order mark opening a file is skipped. Anywhere else in a file, comments and strings included, a byte-order mark, a NUL or another control character but tab, a noncharacter (U+FFFE, U+FFFF, ...) and bytes that are no UTF-8 are a parse error where they stand.
Next
- Events - event contracts, listeners, and the event runtime
- Decorators - the full decorator catalog
- Validators - validation runtime semantics
- Types and Scalars - generics, mixins, advanced types
- AI Reference - one-page consolidated reference (paste this into LLM prompts)