Architecture
craftgo is a design-first framework for Go. You describe your types, their validators, your HTTP services and your event contracts in a small DSL. craftgo gen turns that design into ordinary Go - structs with Validate() methods, net/http handlers, one descriptor per event contract - plus an OpenAPI document. The generated code calls craftgo's runtime packages and your own logic; binding and validation are generated Go, not reflection over struct tags.
The design and your code go in one side; generated packages and the runtime they import come out the other.
You write
Three things, and craftgo never overwrites any of them.
design/<package>/*.craftgo- the design: types, validators, enums, errors, HTTP services, event contracts. See DSL Basics.design/craftgo.design.yaml- the manifest: where each generated artefact lands, and the OpenAPI metadata. See Configuration.- Your Go code - the service stubs craftgo scaffolds for you, your event listeners, and
main.go.
craftgo gen
One command reads the design and writes the output.
craftgo gen designIt walks up from the given path to find craftgo.design.yaml, reads the module path out of go.mod, analyses every .craftgo file under the design root, and writes each artefact to the directory the manifest names. Output is deterministic - the same design always produces byte-identical files - and every Go file goes through go/format, so committing the output and re-running gen in CI is a usable drift gate.
Generated
Four kinds of artefact, each from the same design.
- Types and validation - one folder per design package under
output.types: the structs, theirValidate()methods, the enum constants and the typed errors. A package that declares no type, scalar, enum or error gets no folder, andvalidate.gois written for a package that declares a type or an enum, a scalar with validators, or an error with fields;fill.go, whoseFillEmptysets a required list or map left nil empty before a response or error body is encoded, for a package with a struct that can hold one. - Transport, routes and wiring - one
http.HandlerFuncfactory per method, the per-service route registration, andwiring.Register, the single callmain.gomakes to attach the design to the server. events.go- one file per design package that declares anevent: a contract constant and anevents.Event[T]descriptor per event, and nothing else. Which events a deployable listens to is its own Go.openapi.yaml- the OpenAPI 3.1 document for the HTTP services.
A .proto under the design folder adds the gRPC half: the pb code the protoc plugins write under output.pb, one server package per proto service under output.grpc, and wiring/grpc.go with RegisterGRPC - see gRPC.
Every Go file here opens with // Code generated by craftgo. DO NOT EDIT. (the pb code with the plugins' own header), and openapi.yaml with # Generated by craftgo. DO NOT EDIT.. See Codegen Output for what each one contains.
Runtime
The generated code is written against two packages you import, pkg/server and pkg/events; the scaffolds also use pkg/log and pkg/telemetry, and the gRPC half pkg/rpc.
pkg/serverwraps*http.ServeMux. Routes register with Go 1.22 pattern syntax, middleware is plainfunc(http.Handler) http.Handler, and the built-ins (access log, recovery, CORS, compression, timeouts) are ordinary middleware. See Runtime.pkg/eventsis the event runtime: theBus, the descriptors generated code declares, the middleware chain, andPlan(). It knows nothing about any broker. See Events.- Transport adapters implement the two interfaces the bus needs.
pkg/events/natsandpkg/events/kafkaare modules of their own, so a project on one broker pulls in nothing of the other;pkg/events/memoryships insidepkg/eventsfor tests and single-binary deployments.
Regenerated every run vs written once
Generated files split into two kinds, and the split is the whole workflow.
| Regenerated every run | Written once, then yours |
|---|---|
internal/types/<pkg>/*.go | internal/service/<svc>/<method>.go |
internal/transport/<svc>/<method>.go | internal/middleware/<name>_middleware.go |
internal/routes/routes.go, internal/routes/<svc>/routes.go | svccontext/svccontext.go |
internal/events/<pkg>/events.go | config/config.go, config.yaml, example.config.yaml |
internal/wiring/wiring.go, internal/wiring/grpc.go | main.go |
internal/grpc/<svc>/*.go, internal/pb/<dir>/*.go | |
svccontext/middlewares.go | |
docs/openapi.yaml |
Files in the left column carry the generated header and are overwritten on every run; edit one and the edit is gone. Files in the right column are written only when they are missing, so your logic survives regeneration. At the end of a run craftgo deletes every headed file in its output directories that the run did not write, which is how a renamed service or a deleted event leaves nothing behind.
The exact contents of each file are in Codegen Output.
Next
- Project Structure - the layout on disk.
- Events - contracts, the bus, groups and transports.
- Configuration - the manifest and the runtime config.