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 a link view. Links are listed under the Links function in the media type definition. Views may then use the special links 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:

  1. The default media type is used for all responses that return status code 200 and do not define a media type.
  2. 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.