Skip to content

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 writeWhere
DSL filesdesign/<package>/*.craftgo
Codegen configdesign/craftgo.design.yaml
Business logicinternal/service/<service>/<method>.go
Middleware implementationsinternal/middleware/<name>_middleware.go
Custom config fieldsconfig/config.go
Runtime valuesconfig/config.yaml
What craftgo writesRegenerated 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.yamlyes
internal/service/gen-once
internal/middleware/gen-once
svccontext/svccontext.gogen-once
config/gen-once
main.gogen-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.sum

A 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.go
  • internal/types/<pkg>/validate.go
  • internal/types/<pkg>/errors.go
  • internal/types/<pkg>/enums.go
  • internal/transport/<svc>/<method>.go
  • internal/transport/<svc>/bodies.gen.go
  • internal/routes/routes.go
  • internal/routes/<svc>/routes.go
  • internal/events/<pkg>/events.go
  • internal/pb/<dir>/*.pb.go (written by the protoc plugins, swept by craftgo)
  • internal/grpc/<svc>/server.go and internal/grpc/<svc>/<rpc>.go
  • internal/wiring/wiring.go and internal/wiring/grpc.go
  • svccontext/middlewares.go (sits next to svccontext.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 stub
  • internal/middleware/<name>_middleware.go - middleware stub
  • svccontext/svccontext.go - dependency container
  • config/config.go - config struct + loader
  • config/config.yaml - local config values
  • config/example.config.yaml - committed reference
  • main.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.Register

See Events for what goes in each half.

Configuration ​

design/craftgo.design.yaml controls codegen output. The defaults match the layout above:

yaml
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: /api

Override any path to relocate the corresponding artifact. See Configuration for the full reference.

Force-regenerate ​

To regen from scratch:

bash
rm -rf internal/types internal/transport internal/routes docs/openapi.yaml
craftgo gen design

Gen-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 orders

Cross-package types are referenced by qualifying with the package name - no import needed:

craftgo
package users

type User {
    contact shared.Contact
}

The codegen wires the matching Go imports automatically. Each DSL package produces internal/types/<package>/.

Released under the MIT License.