Why Design-first
Most Go web frameworks are code-first. You write Go structs with tags, register handlers, and the API shape lives across many files. craftgo flips that: write the API spec first, generate the rest.
The problem with code-first
In a typical Gin or Echo project, the API surface lives in three places:
- Go structs with tags - request/response shapes, validation rules, JSON field names
- Handler code - HTTP method, path, middleware
- OpenAPI spec - usually a separate
swagger.yamlyou maintain by hand
These three drift apart. You add a field to the struct but forget to update the OpenAPI. You change a path in the handler but the spec still shows the old route. Your TypeScript clients break in production.
The design-first answer
craftgo collapses the three sources into one. The DSL is the contract. Everything else generates from it.
type User {
id string
name string @length(1, 80)
email string @format(email)
}
@prefix("/v1")
service UserService {
post CreateUser /users {
request CreateUserReq
response User
}
}From this one definition you get:
- Go struct
Userwith field tags Validate()method that enforces the rules- HTTP handler at
POST /v1/users - OpenAPI spec entry for the endpoint
- Component schema in OpenAPI for
User
Add a field once, in the DSL. Everything updates.
What you do not write
json:"name,omitempty"tags- Manual
if req.Name == "" { return ... }checks - Hand-rolled OpenAPI YAML
- Route registration boilerplate
- Type definitions duplicated across HTTP handler and OpenAPI
What you do write
- The DSL file (the contract)
- Business logic in
internal/service/...(your code)
The line between framework and your code is sharp. Generated files start with // Code generated by craftgo. DO NOT EDIT.. Logic stubs are scaffold-once and stay yours forever.
Tooling that comes with the contract
Because the DSL is structured, tools can read it:
- LSP: completion of decorators based on field type, hover that shows decorator documentation, diagnostics that catch type mismatches before code generation.
- Codegen drift detection:
make gen-diffre-runs codegen and fails the build if the working tree changed. Catches "I forgot to regen" in CI. - Spec validation: the generated OpenAPI is valid OAS 3.1. Pass it to
spectral lintorredoclyfor further checks.
When code-first is better
Design-first does not fit every project:
- One-off scripts where the API surface is tiny.
- Highly dynamic dispatch where routes are computed at runtime.
- Glue code wrapping an external service whose schema is fluid.
For typical CRUD-shaped services with a stable contract - most production services - design-first wins on consistency, refactor speed, and onboarding clarity.
Eject is always available
The DSL produces plain Go. If you decide craftgo is not the right tool a year from now, the generated files are idiomatic and can be hand-maintained. There is no runtime hook into framework-internal types. Drop the DSL, keep the Go.