Upgrading from Goa v1 or Goa v2 to v3


Upgrading from v2 to v3

v2 and v3 are functionally equivalent making the upgrade pretty straightforward. v3 requires Go module support and therefore Go 1.11 or higher. Upgrading from v2 to v3 is as simple as:

  • Enabling Go modules on your project (env GO111MODULE=on go mod init)
  • Updating the import path of the goa package to goa.design/goa/v3/pkg
  • Updating the import path of Goa package X from goa.design/goa/X to goa.design/goa/v3/X

That’s it! Note also that the goa tool in v3 is backwards compatible and is able to generate code for v2 design. This makes it possible to work concurrently on both v2 and v3 projects by keeping v2 in the GOPATH and using v3 as a Go module.

Upgrading from v1 to v2 or v3

Goa v2 and v3 bring a host of new features and improvements over v1, most notably:

  • a modular architecture with clear separation of concerns between transport and business logic
  • support for gRPC
  • fewer external dependencies
  • more powerful plugin system including Go kit support

Some of the changes are fairly fundamental and affect how to design services however the basic principles and value proposition remain identical:

  • a single source of truth provided by a Go based design DSL
  • a code generator tool that given the DSL generates documentation, server and client code

This document describes the changes and provides some guidelines on how to upgrade.

Note: Goa v2 and v3 are functionally equivalent, the only difference being that v3 leverages and supports Go modules while v2 does not. The rest of this document refers to v3 but is applicable to both.

Changes to the DSL

Goa v3 promote a clean separation of layers by making it possible to design the service APIs independently of the underlying transport. Transport specific DSL then makes it possible to provide mappings for each transport (HTTP and gRPC). So instead of Resources and Actions the DSL focuses on Services and Methods. Each method describes its input and output types. Transport specific DSL then describes how the input types are built from HTTP request or inbound gRPC messages and how output types should be written to HTTP responses or outbound gRPC messages.

NOTE: The v3 DSL is documented extensively in godoc

Types

For the most part the DSL used to describes types remains the same with a few differences:

  • MediaType is now ResultType to make it clear that types described using this DSL are intended to be used as method results. Note that standard types defined using the Type DSL can also be used as result types.
  • Result types may omit defining views. If a result type does not define a view then a default view is automatically defined that lists all the result type attributes.
  • The new Field DSL is identical to Attribute but makes it possible to specify an index for the field corresponding to the gRPC field number.
  • HashOf is now MapOf which is more intuitive to Go developers.
  • There are new primitive types to describe more precisely the binary layout of the data: Int, Int32, Int64, UInt, UInt32, UInt64, Float32, Float64 and Bytes.
  • The types DateTime and UUID are deprecated in favor of String and corresponding Format validations.

Example

v1 media type:

var Person = MediaType("application/vnd.goa.person", func() {
	Description("A person")
	Attributes(func() {
		Attribute("name", String, "Name of person")
		Attribute("age", Integer, "Age of person")
		Required("name")
	})
	View("default", func() {  // View defines a rendering of the media type.
		Attribute("name") // Media types may have multiple views and must
		Attribute("age")  // have a "default" view.
	})
})

Corresponding v3 result type:

var Person = ResultType("application/vnd.goa.person", func() {
	Description("A person")
	Attributes(func() {
		Attribute("name", String, "Name of person")
		Attribute("age", Int, "Age of person")
		Required("name")
	})
})

Corresponding v3 result type with explicit field indeces:

var Person = ResultType("application/vnd.goa.person", func() {
	Description("A person")
	Attributes(func() {
		Field(1, "name", String, "Name of person")
		Field(2, "age", Int, "Age of person")
		Required("name")
	})
})

API

The following changes have been made to the API DSL:

  • The Host, Scheme and BasePath DSLs are replaced with Server.
  • The Server DSL makes it possible to define server properties for different environments. Each server may list the services it hosts making it possible to define multiple servers in one design.
  • Origin is now implemented as part of the CORS plugin.
  • ResponseTemplate and Trait have been deprecated.

Example

v1 API section:

var _ = API("cellar", func() {
	Title("Cellar Service")
	Description("HTTP service for managing your wine cellar")
	Scheme("http")
	Host("localhost:8080")
	BasePath("/cellar")
})

Corresponding v3 section:

var _ = API("cellar", func() {
	Title("Cellar Service")
	Description("HTTP service for managing your wine cellar")
	Server("app", func() {
		Host("localhost", func() {
			URI("http://localhost:8080/cellar")
		})
	})
})

Corresponding v3 section using multiple servers:

var _ = API("cellar", func() {
	Title("Cellar Service")
	Description("HTTP service for managing your wine cellar")
	Server("app", func() {
		Description("App server hosts the storage and sommelier services.")
		Services("sommelier", "storage")
		Host("localhost", func() {
			Description("default host")
			URI("http://localhost:8080/cellar")
		})
	})
	Server("swagger", func() {
		Description("Swagger server hosts the service OpenAPI specification.")
		Services("swagger")
		Host("localhost", func() {
			Description("default host")
			URI("http://localhost:8088/swagger")
		})
	})
})

Services

The Resource function is now called Service. The DSL is now organized into a transport agnostic section and transport specific DSLs. The transport agnostic section lists the potential errors returned by all the service methods. The transport specific sections then map these errors to HTTP status code or gRPC response codes.

  • BasePath is now called Path and appears in the HTTP DSL.
  • CanonicalActionName is now called CanonicalMethod and appears in the HTTP DSL.
  • Response is replaced with Error.
  • Origin is now implemented as part of the CORS plugin.
  • DefaultMedia is deprecated.

Example

v1 design:

	Resource("bottle", func() {
		Description("A wine bottle")
		BasePath("/bottles")
		Parent("account")
		CanonicalActionName("get")

		Response(Unauthorized, ErrorMedia)
		Response(BadRequest, ErrorMedia)
		// ... Actions
	})

Equivalent v3 design:

	Service("bottle", func() {
		Description("A wine bottle")
		Error("Unauthorized")
		Error("BadRequest")

		HTTP(func() {
			Path("/bottles")
			Parent("account")
			CanonicalMethod("get")
		})
		// ... Methods
	})

Methods

The Action function is replaced by Method. As with services the DSL is organized into a transport agnostic section and transport specific DSLs. The transport agnostic section defines the payload and result types as well as all the possible method specific errors not already defined at the service level. The transport specific DSLs then map the payload and result type attributes to transport specific constructs such as HTTP headers, body etc.

  • Most of the DSL present in v1 is HTTP specific and thus moved to the HTTP DSL.
  • The Param and Header functions now need only list the names of attributes of the corresponding method payload or result types.
  • Error responses now use the Error DSL.
  • HTTP path parameters are now defined using curly braces instead of colons: /foo/{id} instead of /foo/:id.

The mapping of input and output types

v1 action design example:

	Action("update", func() {
		Description("Change account name")
		Routing(
			PUT("/:accountID"),
		)
		Params(func() {
			Param("accountID", Integer, "Account ID")
		})
		Payload(func() {
			Attribute("name", String, "Account name")
			Required("name")
		})
		Response(NoContent)
		Response(NotFound)
		Response(BadRequest, ErrorMedia)
	})

Equivalent v3 design:

	Method("update", func() {
		Description("Change account name")
		Payload(func() {
			Attribute("accountID", Int, "Account ID")
			Attribute("name", String, "Account name")
			Required("name")
		})
		Result(Empty)
		Error("NotFound")
		Error("BadRequest")

		HTTP(func() {
			PUT("/{accountID}")
		})
	})