Code Generation

Markdown
Complete guide to Goa’s code generation - commands, process, generated code structure, and customization options.

Goa’s code generation transforms your design into production-ready service contracts, transports, clients, and documentation. goa example creates runnable starter wiring, while your application supplies the business logic.

Command Line Tools

Installation

go get goa.design/goa/v3@v3.31.1
go install goa.design/goa/v3/cmd/goa@v3.31.1

Commands

All commands expect Go package import paths, not filesystem paths:

# ✅ Correct: using Go package import path
goa gen goa.design/examples/calc/design

# ❌ Incorrect: using filesystem path
goa gen ./design

Generate Code (goa gen)

goa gen <design-package-import-path> [-o <output-dir>]

The primary command for code generation:

  • Processes your design package and generates implementation code
  • Recreates the entire gen/ directory from scratch each time
  • Run after every design change

Create Example (goa example)

goa example <design-package-import-path> [-o <output-dir>]

A scaffolding command:

  • Creates a one-time example implementation
  • Generates handler stubs with example logic
  • Run once when starting a new project
  • Will NOT overwrite existing custom implementation

Show Version

goa version

Development Workflow

  1. Create initial design
  2. Run goa gen to generate base code
  3. Run goa example to create implementation stubs
  4. Implement your service logic
  5. Run goa gen after every design change

Best Practice: Commit generated code to version control rather than generating during CI/CD. This ensures reproducible builds and allows tracking changes in generated code.


Generation Process

When you run goa gen, Goa follows a systematic process:

1. Bootstrap Phase

Goa creates a temporary main.go that:

  • Imports Goa packages and your design package
  • Runs DSL evaluation
  • Triggers code generation

2. Design Evaluation

  • DSL functions execute to create expression objects
  • Expressions combine into a complete API model
  • Relationships between expressions are established
  • Design rules and constraints are validated

3. Code Generation

  • Validated expressions pass to code generators
  • Templates render to produce code files
  • Output writes to the gen/ directory

Goa resolves packages, declarations, names, imports, field paths, and known branches from the complete validated design before rendering. Templates write those choices directly. Generated programs branch only on values that arrive at runtime.


Generated Code Structure

A typical generated project:

myservice/
├── cmd/                    # Generated example commands
│   └── calc/
│       ├── grpc.go
│       └── http.go
├── design/                 # Your design files
│   └── design.go
├── gen/                    # Generated code (don't edit)
│   ├── calc/               # Service-specific code
│   │   ├── client.go
│   │   ├── endpoints.go
│   │   └── service.go
│   ├── http/               # HTTP transport layer
│   │   ├── calc/
│   │   │   ├── client/
│   │   │   └── server/
│   │   └── openapi.json
│   └── grpc/               # gRPC transport layer
│       └── calc/
│           ├── client/
│           ├── server/
│           └── pb/
└── myservice.go            # Your service implementation

Service Interfaces

Generated in gen/<service>/service.go:

// Service interface defines the API contract
type Service interface {
    Add(context.Context, *AddPayload) (res int, err error)
    Multiply(context.Context, *MultiplyPayload) (res int, err error)
}

// Payload types
type AddPayload struct {
    A int32
    B int32
}

// Constants for observability
const ServiceName = "calc"
var MethodNames = [2]string{"add", "multiply"}

Endpoint Layer

Generated in gen/<service>/endpoints.go:

// Endpoints wraps service methods in transport-agnostic endpoints
type Endpoints struct {
    Add      goa.Endpoint
    Multiply goa.Endpoint
}

// NewEndpoints creates endpoints from service implementation
func NewEndpoints(s Service) *Endpoints {
    return &Endpoints{
        Add:      NewAddEndpoint(s),
        Multiply: NewMultiplyEndpoint(s),
    }
}

// Use applies middleware to all endpoints
func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) {
    e.Add = m(e.Add)
    e.Multiply = m(e.Multiply)
}

Endpoint middleware example:

func LoggingMiddleware(next goa.Endpoint) goa.Endpoint {
    return func(ctx context.Context, req any) (res any, err error) {
        log.Printf("request: %v", req)
        res, err = next(ctx, req)
        log.Printf("response: %v", res)
        return
    }
}

endpoints.Use(LoggingMiddleware)

Client Code

Generated in gen/<service>/client.go:

// Client provides typed methods for service calls
type Client struct {
    AddEndpoint      goa.Endpoint
    MultiplyEndpoint goa.Endpoint
}

func NewClient(add, multiply goa.Endpoint) *Client {
    return &Client{
        AddEndpoint:      add,
        MultiplyEndpoint: multiply,
    }
}

func (c *Client) Add(ctx context.Context, p *AddPayload) (res int, err error) {
    ires, err := c.AddEndpoint(ctx, p)
    if err != nil {
        return
    }
    return ires.(int), nil
}

HTTP Code Generation

Server Implementation

Generated in gen/http/<service>/server/server.go:

func New(
    e *calc.Endpoints,
    mux goahttp.Muxer,
    decoder func(*http.Request) goahttp.Decoder,
    encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
    errhandler func(context.Context, http.ResponseWriter, error),
    formatter func(ctx context.Context, err error) goahttp.Statuser,
) *Server

// Server exposes handlers for modification
type Server struct {
    Mounts   []*MountPoint
    Add      http.Handler
    Multiply http.Handler
}

// Use applies HTTP middleware to all handlers
func (s *Server) Use(m func(http.Handler) http.Handler)

Complete server setup:

func main() {
    svc := calc.New()
    endpoints := gencalc.NewEndpoints(svc)
    mux := goahttp.NewMuxer()
    server := genhttp.New(
        endpoints,
        mux,
        goahttp.RequestDecoder,
        goahttp.ResponseEncoder,
        nil, nil)
    genhttp.Mount(mux, server)
    http.ListenAndServe(":8080", mux)
}

Client Implementation

Generated in gen/http/<service>/client/client.go:

func NewClient(
    scheme string,
    host string,
    doer goahttp.Doer,
    enc func(*http.Request) goahttp.Encoder,
    dec func(*http.Response) goahttp.Decoder,
    restoreBody bool,
) *Client

Complete client setup:

func main() {
    httpClient := genclient.NewClient(
        "http",
        "localhost:8080",
        http.DefaultClient,
        goahttp.RequestEncoder,
        goahttp.ResponseDecoder,
        false,
    )

    client := gencalc.NewClient(
        httpClient.Add(),
        httpClient.Multiply(),
    )

    result, err := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
}

gRPC Code Generation

Protobuf Definition

Generated in gen/grpc/<service>/pb/:

syntax = "proto3";
package calc;

service Calc {
    rpc Add (AddRequest) returns (AddResponse);
    rpc Multiply (MultiplyRequest) returns (MultiplyResponse);
}

message AddRequest {
    int64 a = 1;
    int64 b = 2;
}

Server Implementation

func main() {
    svc := calc.New()
    endpoints := gencalc.NewEndpoints(svc)
    svr := grpc.NewServer()
    gensvr := gengrpc.New(endpoints, nil)
    genpb.RegisterCalcServer(svr, gensvr)
    lis, _ := net.Listen("tcp", ":8080")
    svr.Serve(lis)
}

Client Implementation

func main() {
    conn, _ := grpc.Dial("localhost:8080",
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    defer conn.Close()

    grpcClient := genclient.NewClient(conn)
    client := gencalc.NewClient(
        grpcClient.Add(),
        grpcClient.Multiply(),
    )

    result, _ := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
}

Customization

Type Generation Control

Force generation of types not directly referenced by methods:

var MyType = Type("MyType", func() {
    // Force generation in specific services
    Meta("type:generate:force", "service1", "service2")
    
    // Or force generation in all services
    Meta("type:generate:force")
    
    Attribute("name", String)
})

Package Organization

Generate types in a shared package:

var CommonType = Type("CommonType", func() {
    Meta("struct:pkg:path", "types")
    Meta("type:generate:force")
    Attribute("id", String)
})

Creates:

gen/
└── types/
    └── common_type.go

struct:pkg:path gives the authored type one declaration in the selected generated package, and every generated use imports that declaration. The Go package name is the lowercase final path segment. If the relocated type contains another authored type, that dependency must also declare an explicit struct:pkg:path, usually the same package. Compiler-created nested types stay with their owning authored type.

One authored declaration is reused across services and across payload, result, and error uses. When that exact type is a custom error, Goa adds the error methods beside the same declaration instead of generating a second type.

Field Customization

var Message = Type("Message", func() {
    Attribute("id", String, func() {
        // Override field name
        Meta("struct:field:name", "ID")
        
        // Add custom struct tags
        Meta("struct:tag:json", "id,omitempty")
        Meta("struct:tag:msgpack", "id,omitempty")
        
        // Override type
        Meta("struct:field:type", "bson.ObjectId", "github.com/globalsign/mgo/bson", "bson")
    })
})

Protocol Buffer Customization

var MyType = Type("MyType", func() {
    // Override protobuf message name
    Meta("struct:name:proto", "CustomProtoType")
    
    Field(1, "status", Int32, func() {
        // Override protobuf field type
        Meta("struct:field:proto", "int32")
    })

    // Use Google's timestamp type
    Field(2, "created_at", String, func() {
        Meta("struct:field:proto", 
            "google.protobuf.Timestamp",
            "google/protobuf/timestamp.proto",
            "Timestamp",
            "google.golang.org/protobuf/types/known/timestamppb")
    })
})

// Specify protoc include paths
var _ = API("calc", func() {
    Meta("protoc:include", "/usr/include", "/usr/local/include")
})

OpenAPI Customization

By default Goa generates OpenAPI 2.0 and 3.0 documents. To also generate an OpenAPI 3.2.0 description, select it explicitly at the API level:

var _ = API("MyAPI", func() {
    Meta("openapi:versions", "2.0", "3.0", "3.2")
    Meta("openapi:path:3.2", "docs/openapi")
})

The selected versions are written as both JSON and YAML. The example above generates gen/docs/openapi.json and gen/docs/openapi.yaml for OpenAPI 3.2; without the path override, Goa writes gen/http/openapi3.2.json and gen/http/openapi3.2.yaml. Version selection does not change the generated service code.

var _ = API("MyAPI", func() {
    // Control generation
    Meta("openapi:generate", "false")
    
    // Format JSON output
    Meta("openapi:json:prefix", "  ")
    Meta("openapi:json:indent", "  ")
    
    // Disable example generation
    Meta("openapi:example", "false")
})

var _ = Service("UserService", func() {
    // Add tags
    HTTP(func() {
        Meta("openapi:tag:Users")
        Meta("openapi:tag:Backend:desc", "Backend API Operations")
    })
    
    Method("CreateUser", func() {
        // Custom operation ID
        Meta("openapi:operationId", "{service}.{method}")
        
        // Custom summary
        Meta("openapi:summary", "Create a new user")
        
        HTTP(func() {
            // Add extensions
            Meta("openapi:extension:x-rate-limit", `{"rate": 100}`)
            POST("/users")
        })
    })
})

var User = Type("User", func() {
    // Override type name in OpenAPI spec
    Meta("openapi:typename", "CustomUser")
})

Types and Validation

Validation Enforcement

Goa’s generated transport decoders validate incoming requests on the server and incoming responses on the client. Service code then maintains the application invariants. Direct calls to a service or generated endpoint bypass transport decoding; those callers must supply valid values or validate at their own input boundary.

Pointer Rules for Struct Fields

Service types and transport types answer different questions. A service type represents a value after validation. A decoded transport type must also record whether an incoming field was absent so generated validation can reject a missing required value without rejecting an explicit zero value.

This table describes ordinary scalar and object fields in Goa v3.31.1. Bytes, Any, and unions have distinct representations; inspect their generated types.

FieldService typeHTTP/JSON-RPC bodyProtobuf request or response
Required primitive or primitive with a defaultValuePointer when decoded for validation; value when encodedPointer for singular fields whose presence must be preserved
Optional primitive without a defaultPointerPointerPointer
ObjectPointerPointerPointer
Array or mapValueValueValue

For HTTP and JSON-RPC, decoded input means a request on the server or a response on the client. Required or defaulted scalar fields use values in encoded client requests and server responses; optional scalars without defaults remain pointers. In protobuf Go structs, required singular booleans, numbers, strings, enums, and their aliases are pointers in both requests and responses so validation can distinguish an omitted field from an explicit zero value. Byte slices remain slices, messages remain pointers, and Goa service structs keep their existing layout.

Example:

type Person struct {
    Name     string             // required, direct value
    Age      *int               // optional, pointer
    Hobbies  []string           // array, no pointer
    Metadata map[string]string  // map, no pointer
}

ArrayOfRequired applies the same presence rule to array elements. Incoming HTTP and JSON-RPC bodies use pointers for primitive elements and primitive aliases so [null] can be rejected. Valid input becomes an ordinary value slice in the service layer, and generated response bodies remain value slices.

Collection Presence

Required("items") and MinLength(1) express different constraints. For JSON, a required collection must be present and non-null, but [] or {} is valid unless a length constraint forbids it. Protobuf repeated and map fields cannot distinguish absent from empty after a wire round trip, so generated validation checks their length and contents rather than presence. Required singular scalars, messages, and oneofs retain their own presence checks.

Do not use nil versus empty Go collections to encode domain operations. Model an operation explicitly when callers must distinguish “leave unchanged” from “replace with empty.”

Default Value Handling

Defaults belong to the design and are applied by generated transport conversions. In preview gRPC decoding, an absent input receives its authored default; an explicit 0, false, or empty value remains explicit. Service-to-protobuf conversion preserves the value supplied by the service and does not replace zero values with defaults.

HTTP conversions have different rules: incoming body constructors apply defaults to missing values, and outgoing body constructors can apply authored defaults to zero-valued service fields. Inspect the generated constructor and decoder for the relevant direction when zero or absence has domain meaning; do not apply one default rule to every transport.


Views and Result Types

Views control how result types are rendered in responses.

How Views Work

  1. Define the attributes of each view in the result type.
  2. Goa generates the view representations, conversions, and validators.
  3. A method can select a fixed view in the design. For a dynamic unary result, the generated service method returns the view name with the result; streaming interfaces expose the generated view-selection operation.

Server-Side Response

The generated encoder selects the representation for the chosen view. Attributes outside that view are excluded; required attributes inside it remain part of the contract. Dynamic HTTP views carry the name in the Goa-View response header, and gRPC uses goa-view metadata. Preview JSON-RPC carries a dynamic view as { "view": ..., "body": ... } inside result; unviewed and fixed-view results do not use that envelope.

Client-Side Response

The generated client reads the selected view, decodes its representation, validates that view’s contract, and converts it to the service result. Custom clients must follow the representation of the selected transport and release.

Default View

If no views are defined, Goa adds a “default” view that includes all basic fields.


Plugin System

Goa’s plugin system extends code generation. Plugins can:

  1. Add New DSLs - Additional design language constructs
  2. Modify Generated Code - Inspect and modify files, add new files

Example using the CORS plugin:

import (
    . "goa.design/goa/v3/dsl"
    cors "goa.design/plugins/v3/cors/dsl"
)

var _ = Service("calc", func() {
    cors.Origin("/.*localhost.*/", func() {
        cors.Headers("X-Shared-Secret")
        cors.Methods("GET", "POST")
    })
})

Common plugin use cases:

  • Protocol support (CORS, etc.)
  • Additional documentation formats
  • Custom validation rules
  • Cross-cutting concerns (logging, metrics)
  • Configuration file generation

Released callbacks remain appropriate for plugins that edit generated values or files. A plugin that declares a package-level name must use the factory planning phase so Goa can reserve that name with every other declaration before rendering. See the Code Generation Architecture and the upgrade guide for the detailed plugin contract and migration steps.


See Also

  • DSL Reference — Complete DSL reference for design files
  • HTTP Guide — HTTP transport features and customization
  • gRPC Guide — gRPC transport features and Protocol Buffers
  • Quickstart — Getting started with code generation