The goa API Design Language
The goa API Design Language is a DSL implemented in Go that makes it possible to describe arbitrary microservice APIs. While the main focus is REST based HTTP APIs, the language is flexible enough to describe APIs that follow other methodologies as well. Plugins can extend the core DSL to allow describing other aspects of microservices such as database models, service discovery integrations, failure handlers etc.
Design Definitions
At its core the design language consists of functions that are chained together to describe definitions. The goa design language root definition is the API definition, the DSL to define it looks like this:
import (
. "github.com/goadesign/goa/design"
. "github.com/goadesign/goa/design/apidsl"
)
var _ = API("My API", func() { // "My API" is the name of the API used in docs
Title("Documentation title") // Documentation title
Description("The next big thing") // Longer documentation description
Host("goa.design") // Host used by Swagger and clients
Scheme("https") // HTTP scheme used by Swagger and clients
BasePath("/api") // Base path to all API endpoints
Consumes("application/json") // Media types supported by the API
Produces("application/json") // Media types generated by the API
})
A side note on “dot import” as this question comes up often: the goa API design language is a DSL implemented in Go and is not Go. The generated code or any of the actual Go code in goa does not make use of “dot imports”. Using this technique for the DSL results in far cleaner looking code. It also allows mixing DSLs coming from plugins transparently, moving on…
The DSL makes heavy use of anonymous functions to describe the various definitions recursively.
In the example above the
API
function accepts the name of the API as first argument and an anonymous function as second
argument. This anonymous function also referred to as DSL
in this document defines additional
properties of the API. This pattern (name+DSL) is used by many other DSL functions.
The API function defines the general properties of the API: the title and description used in the documentation, the terms of service (not shown in the example above) the default host and scheme used in the documentation and clients and the base path to all the API endpoints (optionally also corresponding base parameters captured via wildcards defined in the base path).
The function also defines the media types supported by the API. The
Consumes
and
Produces
functions make it possible to define the support media types for requests (Consumes
) and
responses (Produces
) optionally also specifying an encoding package that the generated code
uses to unmarshal request payloads and marshal response bodies.
There are a number of other properties that can be defined in the API function ranging from additional metadata (contact information) to security definitions, CORS policies and response templates. See the reference for the complete list.
API Endpoints
Apart from the root API definition the goa API design language also makes it possible to describe
the actual endpoints together with details on the shape of the requests and responses. The
Resource
function defines a set of related API endpoints - a resource if the API is RESTful. Each
actual endpoint is described using the Action
function. Here is an example of a simple Operands
resource exposing an add
action (API endpoint):
var _ = Resource("Operands", func() { // Defines the Operands resource
Action("add", func() { // Defines the add action
Routing(GET("/add/:left/:right")) // The relative path to the add endpoint
Description("add returns the sum of :left and :right in the response body")
Params(func() { // Defines the request parameters
// found in the URI (wildcards) and querystring
Param("left", Integer, "Left operand") // Defines left parameter as path segment
// captured by :left
Param("right", Integer, "Right operand") // Define right parameter as path segment
// captured by :right
})
Response(OK, "plain/text") // Defines response
})
})
A Resource
function may define any arbitrary number of actions. The resource optionally
defines a common base path, common path parameters and other properties shared by all its
actions. An action may define multiple routes (the Routing
function argument is variadic) in
case the same endpoint can be requested from different paths or using different HTTP methods.
The DSL used to define the action parameters allows for specifying validation rules ranging from minimum and maximum values for integer and number parameters to patterns defined via regular expressions for string parameters:
Param("left", Integer, "Left operand", func() {
Minimum(0) // Do not allow for negative values.
})
The syntax used to define parameters is the Attribute DSL described in the section below.
File Servers
The Files
function makes it possible to define file servers on resources. A file server serves a static file
or all files under a given file path if the route ends with a wildcard starting with *
. The
Files function
optionally accepts a child DSL (anonymous function as last argument) for defining a security scheme.
The syntax is identical to the syntax used to define the security scheme of an action.
The following example defines a public
resource with two file servers, one serving the file
public/swagger/swagger.json
for requests sent to /swagger.json
and the other all files under
public/js/
for requests sent to /js/*filepath
where the value of *filepath
corresponds to the
path of the file relative to /public/js
. The swagger endpoint also defines a security scheme
requiring clients to auth before being able to retrieve the swagger specification.
var _ = Resource("public", func() {
Origin("*", func() { // CORS policy that applies to all actions and file servers
Methods("GET") // of "public" resource
})
Files("/swagger.json", "public/swagger/swagger.json", func() {
Security("basic_auth") // Security scheme implemented by /swagger.json endpoint
})
Files("/js/*filepath", "public/js/") // Serve all files under the public/js directory
})
Data Types
The goa API design language makes it possible to describe arbitrary data types that the API may use to define both request payloads and response media types. The Type function describes a data structure by listing each field using the Attribute function. It can also make use of the ArrayOf function to define arrays or fields that are arrays. Here is an example:
// Operand describes a single operand with a name and an integer value.
var Operand = Type("Operand", func() {
Attribute("name", String, "Operand name", func() { // Attribute name of type string
Pattern("^x") // with regex validation
})
Attribute("value", Integer, "Operand value") // Attribute value of type integer
Required("value") // only value is required
})
// Series represents an array of operands.
var Series = ArrayOf(Operand)
Note that like the API
function the Type
function accepts two arguments: a name and a DSL
describing the type properties. The Type
DSL consists of three functions:
- Description sets the type description.
- Attribute defines a single type field.
- Required lists the required fields: fields that must always be present in instances of the type.
Types can be used to define action payloads (amongst other things):
Action("sum", func() { // Defines the sum action
Routing(POST("/sum")) // The relative path to the add endpoint
Description("sum returns the sum of all the operands in the response body")
Payload(Series) // Defines the action request body shape using the Series
// type defined above.
Response(OK, "plain/text") // Defines a response
})
Attributes
Attributes play a special role in the goa DSL. They are the base used to define data structures. The attribute DSL is used to describe type fields, request parameters, request payloads, response headers, response bodies etc. basically anywhere that requires defining a data structure. The syntax for defining attributes is very flexible allowing to specify as little or as much as needed, the complete definition is:
Attribute(<name>, <type>, <description>, <dsl>)
Only the first argument is required, all other arguments are optional. The default attribute
type is String
. The possible types for attributes are:
Name | Go equivalent | JSON equivalent |
---|---|---|
Boolean |
bool | “true” or “false” |
Integer |
int | number |
Number |
float | number |
String |
string | string |
DateTime |
time.Time | RFC3339 string |
UUID |
uuid.UUID | RFC4122 string |
Any |
interface{} | ? |
Additionally type fields can be defined using ArrayOf
or HashOf
or by using a recursive DSL:
var User = Type("user", func() {
Description("A user of the API")
Attribute("name") // Simple string attribute
Attribute("address", func() { // Nested definition, defines a struct in Go
Attribute("number", Integer, "Street number")
Attribute("street", String, "Street name")
Attribute("city", String, "City")
Required("city") // The address must contain at least a city
})
Attribute("friends", ArrayOf("user"))
Attribute("data", HashOf(String, String))
})
Note the use of the "user"
type name to define the friends
field instead of referring to
the User
type variable. Both syntax are accepted. Using names instead of variable reference
allows for building recursive definitions.
The examples Github repository contains a types
directory with a number of example demonstrating the mapping between design types and generated
code.
Responses
Response Media Types
Looking at responses next, the goa design language MediaType
function describes media types which
represent the shape of response bodies. The definition of a media types is similar to the definition
of types (media types are a specialized kind of type) however there are two properties unique to
media types:
-
Views make it possible to describe different renderings of the same media type. Often times an API uses a “short” representation of a resource in listing requests and a more detailed representation in requests that return a single resource. Views cover that use case by providing a way to define these different representations. A media type definition must define the default view used to render the resources (aptly named
default
). -
Links represent related media types that should be rendered embedded in the response. The view used to render links is
link
which means that media types being linked to must define alink
view. Links are listed under theLinks
function in the media type definition. Views may then use the speciallinks
attribute to render all the links.
Here is an example of a media type definition:
// Results is the media type that defines the shape of the "add" action response.
var Results = MediaType("vnd.application/goa.results", func() {
Description("The results of an operation")
Attributes(func() { // Defines the media type attributes
Attribute("value", Integer, "Results value") // Operation results attribute
Attribute("requester", User) // Requester attribute
})
Links(func() { // Defines the links embedded in the media type
Link("requester") // One link to the requester, will be rendered
// using the "link" view of User media type.
})
View("default", func() { // Defines the default view
Attribute("value") // Includes the "value" field in the default view
Attribute("links") // And render links
})
View("extended", func() { // Defines the extended view
Attribute("value") // Includes the value field
Attribute("requester") // and the requester field
})
})
// User is the media type used to render user resources.
var User = MediaType("vnd.application/goa.users", func() {
Description("A user of the API")
Attributes(func() {
Attribute("id", UUID, "Unique identifier")
Attribute("href", String, "User API href")
Attribute("email", String, "User email", func() {
Format("email")
})
})
View("default", func() {
Attribute("id")
Attribute("href")
Attribute("email")
})
View("link", func() { // The view used to render links to User media types.
Attribute("href") // Here only the href attribute is rendered in links.
})
})
Defining Responses
The Response function is used in action declarations to define a specific potential response. It describes the response status code, the media type if the response contains a body and the headers. Each response must have a unique name in the scope of the action, as with most other DSL functions the name is the first argument. The following DSL defines a response named “NoContent” that uses HTTP status code 204:
Response("NoContent", func() {
Description("This is the response returned in case of success")
Status(204)
})
Note that this example as well as all the other examples in this section do not use response templates and therefore define all the properties of the response including its name. In reality in most cases responses are defined using one of the built-in templates so that for example the response above (minus the description) can be short-circuited to:
Response(NoContent)
Response templates are covered in more details in a section below but before we can cover them we
must first understand how Response
works.
If the response contains a body the corresponding media type is specificed using the
Media function.
This function accepts either the media type identifier or actual media type value as first argument
and optionally the name of the media type view used to render the response body. The view is
optional as the same action may return different views depending on the request state for example.
Here is an example of a response definition for a “OK” response using status code 200 and the
Results
media type:
Response("OK", func() {
Description("This is the success response")
Status(200)
Media(Results)
})
As a convenience the media type of a response can be provided as second argument to the Response
function (this is especially useful when using response templates as described in the corresponding
section below). The above is thus equivalent to:
Response("OK", Results, func() {
Description("This is the success response")
Status(200)
})
Assuming the identifier of Results
is application/vnd.example.results
then the above is
equivalent to:
Response("OK", "application/vnd.example.results", func() {
Description("This is the success response")
Status(200)
})
Note that the media type identifier (application/vnd.example.results
in the example above) may or
may not correspond to the identifier of a media type defined via the MediaType
function. The
generated code uses the Go type []byte
to define the type of the response body when the media
type identifier doesn’t match a media type defined in the design.
If the parent action always returns the default view then the response can be defined as:
Response("OK", func() {
Description("This is the success response")
Status(200)
Media(Results, "default")
})
The response headers are defined using the Headers function. The syntax for defining each header is the same syntax used to define attributes:
Response("OK", func() {
Status(200)
Media(Results, "default")
Headers(func() {
Header("Location", String, "Resource location", func() {
Pattern("/results/[0-9]+")
})
Header("ETag") // assumes String type as with Attribute
})
})
Resource vs. Action Responses
Responses can be defined in two places: as part of a Resource
definition or as part of a Action
definition. Responses defined in a Resource
definition apply to all the resource actions.
In this example all the Operands
actions may return a Unauthorized
response:
var _ = Resource("Operands", func() {
Response("Unauthorized", func() {
Description("Response sent for unauthorized requests")
Status(401)
})
Action("add", func() {
Routing(GET("/add/:left/:right"))
Params(func() {
Param("left", Integer, "Left operand")
Param("right", Integer, "Right operand")
})
Response("OK", Results)
// Response "Unauthorized" is implicit
})
})
Leveraging the Default Media Type
Resources can define a default media type for all actions. Defining a default media type has two effects:
- The default media type is used for all responses that return status code 200 and do not define a media type.
- Attributes defined on action payloads, action params and response media types that match the names of attributes defined on the default media type automatically inherit all their properties from it (description, type, validations etc.).
Consider the following resource definition that uses the Results
media type defined above as
default media type and leverages that to define the add
action OK
response:
var _ = Resource("Operands", func() {
DefaultMedia(Results)
Action("add", func() {
Routing(GET("/add/:left/:right"))
Params(func() {
Param("left", Integer, "Left operand")
Param("right", Integer, "Right operand")
})
Response(OK) // Uses the resource default media type
})
})
Now imagine the Results
media type also returned the initial operands used to compute the sum:
var Results = MediaType("vnd.application/goa.results", func() {
Description("The results of an operation")
Attributes(func() {
Attribute("left", Integer, "Left operand")
Attribute("right", Integer, "Right operand")
Attribute("value", Integer, "Results value")
Attribute("requester", User)
})
View("default", func() {
Attribute("left")
Attribute("right")
Attribute("value")
Attribute("requester")
})
})
The add
action definition could take advantage of that to avoid having to repeat the type and
comments for the left
and right
params:
var _ = Resource("Operands", func() {
DefaultMedia(Results)
Action("add", func() {
Routing(GET("/add/:left/:right"))
Params(func() {
Param("left") // Inherits type and description from default media type
Param("right") // Inherits type and description from default media type
})
Response(OK) // Uses the resource default media type
})
})
As the definition for these values evolve they only need to change in one place.
Response Templates
The goa API design language allows defining response templates at the API level that any action may leverage to define its responses. Such templates may accept an arbitrary number of string arguments to define any of the response properties. Templates are defined with the following syntax:
var _ = API("My API", func() {
ResponseTemplate("created", func(hrefPattern string) { // Defines the "created" template
Description("Resource created") // that takes one argument "hrefPattern"
Status(201) // using status code 201
Headers(func() {
Header("Location", func() { // and contains the "Location" header
Pattern(hrefPattern) // with a regex validation defined by the
// value of the "hrefPattern" argument.
})
})
})
})
A template is then used by simply referring to it by name when defining a response:
Action("sum", func() {
Routing(POST("/accounts"))
Payload(AccountPayload)
Response("created", "^/results/[0-9]+") // Response uses the "created" response template.
})
goa provides response templates for all standard HTTP code that define the status so that it is not required to define templates for the simple case. The name of the built-in templates match the name of the corresponding HTTP status code. For example:
Action("show", func() {
Routing(GET("/:id"))
Response("ok", func() {
Status(200)
Media(AccountMedia)
})
})
is equivalent to:
Action("show", func() {
Routing(GET("/:id"))
Response(OK, AccountMedia) // Uses the built-in "OK" response template that defines status 200
})
Conclusion
There is a lot more to the design language but this overview should have given you a sense for how it works. It doesn’t take long for the language to feel natural which makes it possible to quickly iterate and refine the design. The Swagger specification generated from the design can be shared with stakeholders to gather feedback and iterate. Once finalized goagen generates the API scaffolding, request contexts and validation code from the design thereby baking it into the implementation. The design becomes a living document always up-to-date with the implementation.