Configuration
A craftgo project has two configuration files. They live in different places and serve different stages.
At a glance
| File | Read by | When | Controls |
|---|---|---|---|
<design>/craftgo.design.yaml | craftgo CLI | At gen time | Where generated files land + OpenAPI metadata |
<project>/config/config.yaml | main.go | At boot | Server addr, OTel, metrics, your own custom fields |
The first is read once when you run craftgo gen. The second is read every time the binary starts. Both are gen-once - craftgo writes them when missing and never overwrites your edits.
Codegen config (craftgo.design.yaml)
Lives inside the design folder. The directory containing this file is the design root; its parent is the project root (the directory that holds go.mod).
myproject/
├── design/ design root
│ ├── craftgo.design.yaml ← this file
│ └── users/service.craftgo
├── go.mod
└── internal/ generated, sits at project rootcraftgo init creates a starter file. Default content matches every key's default, so an empty manifest works:
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
securitySchemes:
bearer:
type: http
scheme: bearer
bearerFormat: JWToutput.* paths
All paths are relative to the project root (the parent of the design folder, the directory holding go.mod). Override any of them to relocate the corresponding artifact.
| Key | Default | Kind | Holds |
|---|---|---|---|
types | ./internal/types | directory | One subfolder per design package; types.go, validate.go, enums.go, errors.go |
transport | ./internal/transport | directory | One subfolder per service; <method>.go per method |
routes | ./internal/routes | directory | Per-service routes.go plus an umbrella routes.go |
service | ./internal/service | directory | One subfolder per service; <method>.go per method (gen-once) |
middleware | ./internal/middleware | directory | One file per declared middleware Name (gen-once) |
svccontext | ./svccontext/svccontext.go | file path | Single Go file with the dependency container (gen-once); middlewares.go lands beside it |
openapi | ./docs/openapi.yaml | file path | The generated OpenAPI 3.1 spec |
config | ./config | directory | config.go, config.yaml, example.config.yaml (all gen-once) |
main | ./main.go | file path | The project entry point (gen-once) |
The four "file path" entries point at exact files. The rest are directories where craftgo writes one subfolder per package or service.
File and directory naming (output.fileCase)
The names craftgo derives from your DSL identifiers - the per-method <method>.go handler and service files, the per-service directory, and each middleware file - follow the case set by output.fileCase:
fileCase | method file | service directory | middleware file |
|---|---|---|---|
snake (default) | create_user.go | user_service/ | auth_middleware.go |
kebab | create-user.go | user-service/ | auth-middleware.go |
camel | createUser.go | userService/ | authMiddleware.go |
output:
fileCase: snake # snake (default) | kebab | camelIt affects on-disk names only. URL routes are unaffected (they come from @prefix and the method path), Go package names and identifiers are unchanged, and the types/<package>/ layout - named after the DSL package - is untouched. A @group directory keeps the exact group string you wrote.
Customizing paths
A common use is to put generated artifacts at the project root instead of under internal/:
output:
types: ./types
transport: ./transport
routes: ./routes
service: ./serviceOr split off the spec to a separate api/ folder:
output:
openapi: ./api/openapi.yamlOr skip generating an artifact entirely with -:
output:
main: - # do not generate main.goWhen main: - is set, craftgo also skips config/, svccontext, and middleware since those exist to support main.go. Useful for projects that import the generated types as a library and run their own server.
Module path is auto-resolved
The craftgo.design.yaml does not carry a Go module / package field. craftgo reads module <path> from go.mod (walking up from the project root) at gen time and uses that for every Go import in generated files.
If go.mod is missing, craftgo gen fails with a clear error. Run go mod init <module> first.
openapi.* block
Metadata that flows into the generated openapi.yaml.
| Key | Type | Effect |
|---|---|---|
title | string | OpenAPI document title |
version | string | OpenAPI document version |
basePath | string | Path prefix prepended to every operation path |
securitySchemes | map | Named OpenAPI security schemes (see below) |
version can also be set per-file via @version("...") - file-level decorator wins when present. title is manifest-only.
basePath rides into the servers[0].url field of the generated spec. If you need multiple servers or richer descriptions, edit the generated openapi.yaml after gen (it is committed; craftgo regenerates it on every run).
openapi.securitySchemes
Each entry is a named scheme referenced from the DSL with @security(<name>). Schemes:
openapi:
securitySchemes:
bearer: # @security(bearer)
type: http
scheme: bearer
bearerFormat: JWT
apiKeyHeader: # @security(apiKeyHeader)
type: apiKey
in: header
name: X-API-Key
oauth2: # @security(oauth2)
type: oauth2
openIdConnect:
type: openIdConnect
openIdConnectUrl: https://issuer.example.com/.well-known/openid-configurationSupported type values: http, apiKey, oauth2, openIdConnect, mutualTLS. Per-type extra fields:
http:scheme(e.g.bearer,basic), optionalbearerFormatapiKey:in(header/query/cookie),nameoauth2: scopes are application-defined - validated as strings, not against a fixed setopenIdConnect:openIdConnectUrl
The semantic analyzer cross-checks every @security(<name>) reference against this map. Unknown names fail at gen time, not at deploy.
Runtime config (config/config.yaml)
Generated by craftgo gen on first run alongside config.go. Read by main.go via config.Load(). Default content:
server:
addr: ":8080"
handlerTimeout: 0s
maxBodySize: 0
compression:
enabled: false
minSize: 0
level: 0
logging:
level: info
otel:
enabled: true
serviceName: my-app
exporter: none
endpoint: ""
metrics:
enabled: true
exporter: prometheus
endpoint: ""
adminAddr: ":9090"
path: /metrics
docs:
enabled: true
ui: redoc
path: /docs
specPath: /openapi.yamlserver
| Key | Type | Effect |
|---|---|---|
addr | string | Listen address. ":8080", "127.0.0.1:8080", etc. |
handlerTimeout | duration | Global per-handler deadline. 0s = no global cap; per-method @timeout overrides. |
maxBodySize | int | Global request body cap in bytes. 0 = no cap. |
compression.enabled | bool | Toggle gzip / deflate response compression. |
compression.minSize | int | Skip compression when body is smaller. 0 falls back to 1024. |
compression.level | int | Compression level (1-9). 0 falls back to default. |
Compression is off by default. Turn it on only when not behind a compressing reverse proxy (Nginx, Envoy, CloudFront).
logging
| Key | Effect |
|---|---|
level | Minimum log level: debug / info / warn / error. Default info; unrecognised values keep info. Feeds log.SetLevel in main.go, retuning the server and the generated logic layer together. |
otel
| Key | Effect |
|---|---|
enabled | Toggle the OTel HTTP middleware. |
serviceName | service.name resource attribute on every span. |
exporter | none / stdout / otlp_grpc / otlp_http. |
endpoint | OTLP collector address. Ignored for none / stdout. |
Setting enabled: true with exporter: none produces in-process spans whose IDs flow into log lines but are not exported.
metrics
| Key | Effect |
|---|---|
enabled | Toggle the meter provider and admin scrape listener. |
exporter | prometheus / otlp_grpc / otlp_http / none. |
endpoint | OTLP collector address (ignored for prometheus / none). |
adminAddr | Listen address for /metrics scrape (prometheus only). |
path | URL path for the scrape (default /metrics). |
For otlp_grpc / otlp_http, endpoint may be a bare host:port (plaintext) or a full URL whose scheme selects transport security - http://… (plaintext) or https://… (TLS). exporter: none installs a silent meter (no scrape, no push).
docs
| Key | Effect |
|---|---|
enabled | Serve the OpenAPI document + a rendered docs page (on by default). |
ui | redoc / swagger / scalar - the renderer (assets load from a CDN). |
path | HTML docs page route (default /docs). |
specPath | Raw OpenAPI document route (default /openapi.yaml). |
The admin listener runs separately from the public API listener.
Adding custom fields
Edit config/config.go (gen-once - your edits stick):
type Config struct {
Server ServerConfig `yaml:"server"`
Logging LogConfig `yaml:"logging"`
OTel OTelConfig `yaml:"otel"`
Metrics MetricsConfig `yaml:"metrics"`
Docs DocsConfig `yaml:"docs"`
DB struct {
DSN string `yaml:"dsn"`
} `yaml:"db"`
}Then add the matching block to config.yaml:
db:
dsn: postgres://localhost/myappRead from your service via svcCtx.Config.DB.DSN.
File location at runtime
main.go reads config/config.yaml by default. Pass a different path by editing the call:
cfg, err := config.Load("/etc/myapp/config.yaml")craftgo does not read environment variables. The YAML file is the single source of runtime configuration. Mount the right file per environment:
deploy/
├── config.dev.yaml
├── config.staging.yaml
└── config.production.yamlCI / your deployer copies the right file to config/config.yaml before the binary starts.