Project Structure
A craftgo project keeps the contract under design/ and the generated code under internal/, beside the scaffolds craftgo writes once (svccontext/, config/, main.go) and the document under docs/. You write the DSL files, and fill the stubs and scaffolds the tables below mark as yours.
At a glance
| What you write | Where |
|---|---|
| DSL files | design/<package>/*.craftgo |
| Codegen config | design/craftgo.design.yaml |
| Business logic | internal/service/<service>/<method>.go |
| Middleware implementations | internal/middleware/<name>_middleware.go |
| Custom config fields | config/config.go |
| Runtime values | config/config.yaml |
| What craftgo writes | Regenerated each run? |
|---|---|
internal/types/ | yes |
internal/transport/ | yes |
internal/routes/ | yes |
internal/events/ | yes |
internal/pb/ | yes (by the protoc plugins) |
internal/grpc/ | yes |
internal/wiring/ | yes |
docs/openapi.yaml | yes |
internal/service/ | gen-once |
internal/middleware/ | gen-once |
svccontext/svccontext.go | gen-once |
config/ | gen-once |
main.go | gen-once |
Layout after craftgo gen
my-app/
├── design/ YOU WRITE
│ ├── craftgo.design.yaml codegen config
│ └── users/
│ └── service.craftgo DSL file
│
├── internal/ generated + stubs
│ ├── types/
│ │ └── users/ one subfolder per design package
│ │ ├── types.go generated structs
│ │ ├── validate.go generated Validate() methods
│ │ ├── enums.go generated enum types
│ │ └── errors.go generated typed errors
│ ├── transport/
│ │ └── user_service/ one subfolder per service
│ │ ├── create_user.go generated HTTP handler factory (one per method)
│ │ └── bodies.gen.go generated wire structs of the JSON bodies
│ ├── service/
│ │ └── user_service/
│ │ └── create_user.go gen-once stub - YOU FILL
│ ├── routes/
│ │ ├── routes.go umbrella registration
│ │ └── user_service/routes.go per-service registration
│ ├── events/
│ │ └── orders/events.go one file per design package declaring an event
│ ├── pb/
│ │ └── greet/greet.pb.go protoc-gen-go + protoc-gen-go-grpc output, per .proto
│ ├── grpc/
│ │ └── greeter/ one subfolder per proto service
│ │ ├── server.go implements pb.GreeterServer
│ │ └── say_hello.go one file per RPC, delegates to service/
│ ├── wiring/
│ │ ├── wiring.go the single Register call main.go makes
│ │ └── grpc.go RegisterGRPC, while a proto declares a service
│ └── middleware/
│ └── auth_required_middleware.go gen-once stub per declared middleware
│
├── svccontext/
│ ├── svccontext.go gen-once dependency container
│ └── middlewares.go generated middleware fields
│
├── config/ gen-once
│ ├── config.go Go struct + loader
│ ├── config.yaml runtime values
│ └── example.config.yaml committed reference
│
├── docs/
│ └── openapi.yaml generated OpenAPI spec
│
├── main.go gen-once entry point (HTTP routes or proto services only)
├── go.mod
└── go.sumA package's types/ folder holds enums.go only when it declares an enum and errors.go only when it declares an error; events/, pb/, grpc/ and middleware/ appear when the design declares events, protos and middlewares.
File and directory names derived from your identifiers (user_service/, create_user.go, auth_required_middleware.go) use snake_case by default. Set output.fileCase to kebab or camel to change them; URL routes and Go package names are unaffected.
What is generated every run
These files carry a generated header - // Code generated by craftgo. DO NOT EDIT. in Go, # Generated by craftgo. DO NOT EDIT. in openapi.yaml, the plugins' own headers in the pb code - and are overwritten on every craftgo gen:
internal/types/<pkg>/types.gointernal/types/<pkg>/validate.gointernal/types/<pkg>/errors.gointernal/types/<pkg>/enums.gointernal/transport/<svc>/<method>.gointernal/transport/<svc>/bodies.gen.gointernal/routes/routes.gointernal/routes/<svc>/routes.gointernal/events/<pkg>/events.gointernal/pb/<dir>/*.pb.go(written by the protoc plugins, swept by craftgo)internal/grpc/<svc>/server.goandinternal/grpc/<svc>/<rpc>.gointernal/wiring/wiring.goandinternal/wiring/grpc.gosvccontext/middlewares.go(sits next tosvccontext.go)docs/openapi.yaml
If you edit these files, your changes vanish next regen.
What is gen-once
These files are written when missing and never touched again:
internal/service/<svc>/<method>.go- business logic stubinternal/middleware/<name>_middleware.go- middleware stubsvccontext/svccontext.go- dependency containerconfig/config.go- config struct + loaderconfig/config.yaml- local config valuesconfig/example.config.yaml- committed referencemain.go- entry point
You own these. The framework leaves them alone.
Events layout
A design that declares events usually splits in two: a contracts project several deployables import, and the deployables themselves. The contracts project holds one folder per package and version and generates only payload types and descriptors; each deployable writes its own internal/handler for the groups and Subscribe lines, keeps its HTTP stubs in internal/service, and pins what it listens to in a plan_test.go.
contracts/
├── design/
│ ├── craftgo.design.yaml kind: contracts
│ │ output.types: ./gen/events/types
│ │ events.targets[0].out: ./gen/events/contracts
│ └── theme/
│ ├── v1/theme.craftgo package themev1
│ └── v2/theme.craftgo package themev2
├── gen/events/
│ ├── types/themev1/ REGEN - payload structs + Validate()
│ ├── types/themev2/
│ ├── contracts/themev1/events.go REGEN - contract constants + descriptors
│ └── contracts/themev2/events.go
└── docs/openapi.yaml REGEN - components only, unless output.openapi is "-"
store-front/
├── design/craftgo.design.yaml kind: application
├── internal/
│ ├── handler/theme/handler.go YOU WRITE - groups + Subscribe lines
│ └── service/<svc>/<method>.go gen-once stubs for the HTTP side
├── plan_test.go golden test over bus.Plan()
└── main.go builds the bus, calls handler.RegisterSee Events for what goes in each half.
Configuration
design/craftgo.design.yaml controls codegen output. The defaults match the layout above:
output:
types: ./internal/types
transport: ./internal/transport
routes: ./internal/routes
service: ./internal/service
middleware: ./internal/middleware
svccontext: ./svccontext/svccontext.go
openapi: ./docs/openapi.yaml
config: ./config
main: ./main.go
openapi:
title: My API
version: 1.0.0
basePath: /apiOverride any path to relocate the corresponding artifact. See Configuration for the full reference.
Force-regenerate
To regen from scratch:
rm -rf internal/types internal/transport internal/routes docs/openapi.yaml
craftgo gen designGen-once files (internal/service/, internal/middleware/, svccontext/, config/, main.go) survive rm-and-regen because craftgo only writes them when missing.
Multiple packages
Each package name becomes a separate Go package, whichever folders its files sit in; files that declare the same package share it and reference each other directly. One folder per package is the usual layout.
craftgo reads every .craftgo, .cg and .proto file under the design folder, following links to directories - a package folder several deployables share can be linked in. Like the go command, it passes over hidden directories, vendor and node_modules, and every file whose name starts with . or _: an editor's lock file, a macOS ._ file, a draft set aside.
design/
├── craftgo.design.yaml
├── shared/
│ └── common.craftgo package shared
├── users/
│ ├── service.craftgo package users
│ └── errors.craftgo
└── orders/
└── service.craftgo package ordersCross-package types are referenced by qualifying with the package name - no import needed:
package users
type User {
contact shared.Contact
}The codegen wires the matching Go imports automatically. Each DSL package produces internal/types/<package>/.