Design First API Development with Goa
Goa.design is a golang tool for developing APIs using a design first approach. By leveraging Goa it is possible to generate server and client code automatically, documentation through OpenAPI (version 2 and 3 are supported) as well as gRPC code.
This blog post introduces Goa, it’s concepts and showcases some short examples. It does not walk through the installation, or intend to supersede its tutorial/walkthrough which you should look over, here
Goa is broken into three parts; the design language (DSL), code generation and the Go package itself.
What is a DSL?
A DSL (Domain-Specific Language) is a programming language or a set of rules and syntax specifically designed to solve problems within a particular domain or industry. It provides a higher-level abstraction that allows programmers to express solutions in a more concise and declarative manner.
The above statement sums it up succinctly. In our case the domain or industry specifics relate to HTTP and RPC communication via network calls. A lot of API work is boilerplate and can be hard to implement all the things considered best practise. Goa strives to reduce the amount of programmer work required to build out well crafted APIs.
Instead of writing out an OpenAPI document and then converting it to Go code such as oapi-codegen, Goa uses its DSL to generate it (and the code).
Personally, I find the DSL quite powerful and easy to understand. What’s more, it starts simple but provides rich ways to extend it as your requirements dictate. The creator (raphael) is also very active on github and golang’s slack.
Here’s a snippet of the DSL.
package design
import . "goa.design/goa/v3/dsl"
// API describes the global properties of the API server.
var _ = API("check-redirects", func() {
Title("HTTP Redirection Detection Service")
Description("HTTP service detecting and reporting any and all redirects in a HTTP request")
Server("server", func() {
Host("localhost", func() { URI("http://localhost:9090") })
})
})
var _ = Service("health", func() {
Description("endpoints for determining service uptime and status")
HTTP(func() {
Path("/")
})
Method("healthz", func() {
HTTP(func() {
GET("/healthz")
Response(StatusOK)
})
Result(Empty)
})
Method("version", func() {
HTTP(func() {
GET("/version")
Response(StatusOK)
})
Result(AppVersion)
})
})
var AppVersion = Type("version", func() {
Description("Application version information")
Attribute("version", String, "Application version", func() {
Example("1.0")
Example("6b51bebe0f965a5fffa8ff9db5aa702c76ec47f2")
})
})
Without going too deep right now, this will create an API called server, a service called health and within the health
service build two endpoints; healthz
and version
. A custom type called version
will also be created and used in the
/version
endpoint.
The DSL provides an abstraction which lets you craft your API in a declarative way. One big benefit is its just Go code meaning you can easily extend, simplify or create generic functions when working with it.
Code generation
Writing the DSL by itself does not generate code or documentation. The goa
CLI does that. Specifically, goa gen
.
After installing Goa, you can generate the client and server code.
To generate the code run goa gen
. Typically, Goa suggests a pattern of placing all DSL files into
a directory called design
. You don’t have to do this, but I think it makes sense for most use cases.
For the above code snippet, if it were at this path design/design.go
you would run
goa gen github.com/danielmichaels/checkredirects/design
. In this example github.com/danielmichaels/checkredirects
is
my module that I used during go mod init
.
Example generation
With this directory structure:
.
├── design
│  └── design.go
├── go.mod
└── go.sum
After I run goa gen github.com/danielmichaels/checkredirects/design
, it will create a directory called gen
with the following files:
├── design
│  └── design.go
├── gen # New
│  ├── health
│  │  ├── client.go
│  │  ├── endpoints.go
│  │  └── service.go
│  └── http
│  ├── cli
│  │  └── server
│  │  └── cli.go
│  ├── health
│  │  ├── client
│  │  │  ├── client.go
│  │  │  ├── cli.go
│  │  │  ├── encode_decode.go
│  │  │  ├── paths.go
│  │  │  └── types.go
│  │  └── server
│  │  ├── encode_decode.go
│  │  ├── paths.go
│  │  ├── server.go
│  │  └── types.go
│  ├── openapi3.json
│  ├── openapi3.yaml
│  ├── openapi.json
│  └── openapi.yaml
├── go.mod
└── go.sum
Now we have our Go code which we can import into our services. Note, this is all auto generated and should not be edited as
it’ll be overwritten whenever we run goa gen
.
Creating the services
Now that the package code has been generated we can create the entrypoint, and service files automatically with another command;
goa example
. Running this results in some new files:
.
├── cmd # New
│  ├── server
│  │  ├── http.go
│  │  └── main.go
│  └── server-cli
│  ├── http.go
│  └── main.go
├── design
│  └── design.go
├── gen # Truncated for brevity
├── go.mod
├── go.sum
└── health.go # New
Unlike goa gen
this is a one-shot deal; if the generated files already exist it will not re-create them. This is because all your business logic
will be inside these files and it may override things you don’t want overridden.
If we peek at health.go
it will have stubbed out all the handlers, ready to be populated which your business logic.
package checkredirects
import (
"context"
"log"
health "github.com/danielmichaels/checkredirects/gen/health"
)
// health service example implementation.
// The example methods log the requests and return zero values.
type healthsrvc struct {
logger *log.Logger
}
// NewHealth returns the health service implementation.
func NewHealth(logger *log.Logger) health.Service {
return &healthsrvc{logger}
}
// Healthz implements healthz.
func (s *healthsrvc) Healthz(ctx context.Context) (err error) {
s.logger.Print("health.healthz")
return
}
// Version implements version.
func (s *healthsrvc) Version(ctx context.Context) (res *health.Version2, err error) {
res = &health.Version2{}
s.logger.Print("health.version")
return
}
So after writing only 40 lines (design/design.go
) we were able to auto generate a complete and working web server with
two endpoints.
The responses as expected do not return anything but work as demonstrated below.
$ curlie :9090/healthz
HTTP/1.1 200 OK
Date: Tue, 09 Jan 2024 04:44:21 GMT
Content-Length: 0
$ curlie :9090/version
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 09 Jan 2024 04:44:28 GMT
Content-Length: 3
{
}
Documentation
Another great feature of Goa is how well it documents the code and that it can generate valid OpenAPI documents.
This is the document it created:
openapi: 3.0.3
info:
title: HTTP Redirection Detection Service
description: HTTP service detecting and reporting any and all redirects in a HTTP request
version: 0.0.1
servers:
- url: http://localhost:9090
paths:
/healthz:
get:
tags:
- health
summary: healthz health
operationId: health#healthz
responses:
"200":
description: OK response.
/version:
get:
tags:
- health
summary: version health
operationId: health#version
responses:
"200":
description: OK response.
content:
application/json:
schema:
$ref: '#/components/schemas/Version'
example:
version: 6b51bebe0f965a5fffa8ff9db5aa702c76ec47f2
components:
schemas:
Version:
type: object
properties:
version:
type: string
description: Application version
example: 6b51bebe0f965a5fffa8ff9db5aa702c76ec47f2
example:
version: 6b51bebe0f965a5fffa8ff9db5aa702c76ec47f2
tags:
- name: health
description: endpoints for determining service uptime and status
Goa also provides powerful constructs to enhance the documents. For example to define a field on a schema
we can use
Attribute
or Field
type. For this post we’re only focusing on HTTP which uses Attribute
whereas Field
is for both
HTTP and gRPC.
Example of a Goa Payload
and how we can add more context to it.
// Truncated
Payload(func() {
Attribute("username", String, "Username", func () {
Example("MyUsername")
Pattern("^user_[a-zA-Z0-9]{12}$")
})
Required("password")
})
// Truncated
This will create an OpenAPI document which provides an example of MyUsername
. The Pattern
will also enforce the regex
and automatically handle payload validation without the need to write any logic.
Conclusion
This post sought to introduce Goa in the most simple of terms. It hardly scratches the surface of its capabilities. I chose
the most simplistic example I could because it will lay the groundwork for some follow-up posts which show how to add more
realistic endpoints. In the next post I will create a service which accepts and returns JSON leveraging Goa’s Type
, Error
and Payload
DSL primitives. This will be published with source code.
Keep up to date with my stuff
Subscribe to get new posts and retrospectives