API, REST, Golang

Creating a URL Shortener API with the Goa Golang Framework

Goa is an excellent framework for creating APIs and microservices in Golang. Unlike other frameworks, Goa has a DSL to help you programmatically describe your entire API. In doing so, it gives you Swagger documentation for consuming your API for free. You can read more about this aspect of Goa in my blog post on APIs and Swagger. In this post, I am going to be putting Goa in action and creating a URL shortener REST API. I will only be going over one endpoint here but you can find all the code which didn't make it into this post in my this GitHub repository.

When you start creating an API with Gpa, you generally start with a design/ folder. This is where the DSL files go, which will describe your API and generate the code surrounding it. The generated code will handle validation, URL parsing, model generation, etc... Goas goal is to take the tedious tasks associated with creating APIs and use code generation to build that out for you. So let's get started!

Design

The design folder can have as many, or little files as you want. I like to break it up into a couple files because they can get pretty long. This is the structure I normally use:

  1. api.go
  2. resources.go
  3. responses.go
  4. payloads.go

Let's create these in order. Before you start you will need to get the Goa code with this command:

go get -u github.com/goadesign/goa/...

And each file in the design directory will need these imports:

import (
  . "github.com/goadesign/goa/design"
  . "github.com/goadesign/goa/design/apidsl"
)  

The API design file describes your API at a high level for the generated Swagger documentation. It also defines the host, CORS, and base path for the API.
api.go

var _ = API("URL Shortener API", func() {
    Title("The URL Shortener API")
    Description("An API for a URL Shortener")
    Contact(func() {
        Name("Example")
        Email("[email protected]")
    })
    Host("localhost:8080")
    Scheme("http")
    BasePath("/api/")
    Origin("*", func() {
        Headers("Content-Type")
        Methods("GET", "POST", "PATCH", "DELETE", "PUT", "OPTION")
    })
    Consumes("application/json")
    Produces("application/json")
})

The resources file describes the routes for the API. It defines what payload, parameters, and responses each endpoint in your API expect.
resources.go

    Action("create", func() {
        Routing(
            POST("/s"),
        )
        Description("Create a url")
        Payload(CreateLinkPayload)
        Response(Created)
        Response(InternalServerError)
    })

    Action("get", func() {
        Routing(
            GET("/s/:path"),
        )
        Description("Get a url")
        Params(func() {
            Param("path", String, "Path", func() {
                Example("path")
            })
        })
        Response(OK, Url)
        Response(InternalServerError)
        Response(NotFound)
    })
    
    ...

    Action("analytics", func() {
        Routing(
            GET("/s/:path/analytics"),
        )
        Description("Get analytics for a url")
        Params(func() {
            Param("path", String, "Path", func() {
                Example("path")
            })
        })
        Response(OK, Analytics)
        Response(InternalServerError)
        Response(NotFound)
    })
})

The responses file defines models which you will be returning from your API. So in this case we will be returning a URL model and a analytics model.
responses.go

var Url = MediaType("application/vnd.url+json", func() {
    Description("A URL")
    Attributes(func() {
        Attribute("id", Integer, "URL ID", func() {
            Example(1)
        })
        Attribute("url", String, "External url", func() {
            Example("example.com")
        })
        Attribute("path", String, "URL path key", func() {
            Example("path")
        })
    })

    View("default", func() {
        Attribute("id")
        Attribute("url")
        Attribute("path")
    })
})

var Analytics = MediaType("application/vnd.analytics+json", func() {
    Description("Url analytics")
    Attributes(func() {
        Attribute("hits", Integer, func() {
            Example(1)
        })
    })

    View("default", func() {
        Attribute("hits")
    })
})

The final file which needs to be defined is the payloads file. This one is convenient because it handles validation for your requests. You can tell it what type you expect each parameter to be, as well as you can define regex, min, max lengths, etc... This cleans up your controllers greatly and results in much more readable code.
payloads.go

var CreateLinkPayload = Type("CreateLinkPayload", func() {
    Attribute("path", String, func() {
        MinLength(6)
        MaxLength(100)
        Example("path")
    })

    Attribute("url", String, func() {
        MinLength(8)
        MaxLength(2000)
        Example("path")
    })
    Required("path", "url")
})

var UpdateLinkPayload = Type("UpdateLinkPayload", func() {
    Attribute("path", String, func() {
        MinLength(6)
        MaxLength(100)
        Example("path")
    })

    Attribute("url", String, func() {
        MinLength(8)
        MaxLength(2000)
        Example("path")
    })
    Required("path", "url")
})

Generating the Code

Now that you have the API defined, the next step is to generate the scaffolding. You can do that by running:
goagen bootstrap -d github.com/rymccue/goa-url-shortener-api/design
This will generate the code surrounding the controllers, leaving you with the less tedious work of hooking up the endpoints.

Utils

Now that we have the code generated we need to get a database connection, without that there isn't much we can do... I generally make a utils folder and put helpers like the code below there.
utils/database/database.go

func Connect(user, password, dbname, host, port string) (*sql.DB, error) {
    connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%s",
        user, password, dbname, host, port)
    return sql.Open("postgres", connStr)
}

Main

The main.go file is built when the code is generated, there are two changes that need to be made to this file though, first is adding the database connection.

db, err := database.Connect(os.Getenv("PGUSER"), os.Getenv("PGPASS"), "url_shortener", os.Getenv("PGHOST"), os.Getenv("PGPORT"))
    if err != nil {
        service.LogError("startup", "err", err)
    }

Second, we will modify the controller in the next section, but we'll jump ahead and pass the database into the new shortener controller function.

c := NewShortenerController(service, db)

Controller

Now that the main file is complete it's on to the shortener controller. The file is located in the base directory in the shortener.go file. The controller needs a database so first, we add that to the structure.

// ShortenerController implements the shortener resource.
type ShortenerController struct {
    *goa.Controller
    DB *sql.DB
}

Next we alter the new shortener controller function and add a database parameter to the function, and pass it into the shortener controller.

// NewShortenerController creates a shortener controller.
func NewShortenerController(service *goa.Service, db *sql.DB) *ShortenerController {
    return &ShortenerController{
        Controller: service.NewController("ShortenerController"),
        DB:         db,
    }
}

Now that the controller struct and function are created, lets create an action, I'm only creating one here but the rest are in the github repository.
The request body in Goa is accessed ctx.Payload so we assign that to p.

// Create runs the create action.
func (c *ShortenerController) Create(ctx *app.CreateShortenerContext) error {
    // ShortenerController_Create: start_implement
    p := ctx.Payload

After that we access a repository, this will be created later. The CreateURL function creates a URL and saves it to the database. If there is an error, we return at internal server error to the client.

    err := repositories.CreateURL(c.DB, p.URL, p.Path)
    if err != nil {
        c.Service.LogError("Create URL", "err", err)
        return ctx.InternalServerError()
    }

The final step is to return a created status to the client.

    // ShortenerController_Create: end_implement
    return ctx.Created()
}

Repository

The final piece of this created action is the repository. Above we reference a CreateURL function, let's create it now. We pass the database, URL, and path into the function, and create an insert SQL query. Then, we execute it by passing the SQL string, along with the URL and path into the Exec function. Finally, we return the error given to us by the query execution in case something went wrong.

func CreateURL(db *sql.DB, url, path string) error {
    const query = `
        insert into urls (
            url,
            path
        ) values (
            $1,
            $2
        )
    `
    _, err := db.Exec(query, url, path)
    return err
}

Final Thoughts

Goa is an excellent framework for writing, clean, elegant, and well thought out APIs in Golang. I believe this is the best framework out there for those reasons and the fact that it can be completely customized to fit your needs. This post is designed to show you a simple example of what you can do with it, but it has far more power.

Author image

About Ryan McCue

Hi, my name is Ryan! I am a Software Developer with experience in many web frameworks and libraries including NodeJS, Django, Golang, and Laravel.