Routes and OpenAPI specification

👨‍🏫 Before we start…

  • Resource-oriented design helps to create a predictable, uniform interface for designing and developing APIs. We’ll start by implementing a common interface and designing our APIs based on it.
  • gorilla/mux and go-chi/chi are the popular router packages in the Go ecosystem. We’ll go with go-chi/chi because of its lightweightness and 100% compatibility with net/http.
  • We’ll use swaggo/swag to generate the OpenAPI specification from the annotations in each handler, even though it still supports only OpenAPI 2/ Swagger 2.0 specifications. Packages such as swaggest/rest, deepmap/oapi-codegen support OpenAPI 3, but these are custom boilerplate generators with/ from OpenAPI 3 specifications.

Resource oriented design

🔍 Resource oriented architecture is a style of software architecture and programming paradigm for supportively designing and developing software in the form of inter-networking of resources with “RESTful” interfaces, first described by Leonard Richardson and Sam Ruby in their book “RESTful Web Services” in 2007.

Resource oriented design is based on individually named resources (nouns) and their relations with a small number of standard methods (verbs). In this project, we implement a simple RESTful bookshelf API in Go. So, let’s take it as an example.

Functionality Resource Method name HTTP Method Route
API Health health Read GET /livez
List Books book List GET /v1/books
Create Book book Create POST /v1/books
Read Book book Read GET /v1/books/{id}
Update Book book Update PUT /v1/books/{id}
Delete Book book Delete DELETE /v1/books/{id}

As you can see, it creates a predictable, uniform interface for designing and developing the APIs. Our main resource is the book and to implement a CRUD, we use the List, Create, Read, Update, and Delete method names. Aside from that, to check the API’s health, we use the resource health with the Read method.

Also, we’ll save each resource handler under the newly created api/resource folder.

api
 └── resource
    ├── health
    │  └── handler.go
    └── book
       └── handler.go

⭐️ Some web frameworks and ecosystems use List, Create, Get, Update, and Delete method names to implement CRUD. We use Read instead of Get here to avoid confusion with the GET HTTP method on List and Read. Also, if you want to support pagination or need to get the total/ filtered items count, you can align with the List, Count, Create, Read, Update, and Delete method names.

Adding router and routes

Let’s get started with the APIs. We’ll start by adding initial handler functions on each resource inside the api/resource folder, followed by the router implementation in the api/router folder. After that, we’ll update the cmd/api/main.go file to remove the initial “Hello, world!” handler and add the newly created router to our API.

1. Add api/resource/health/handler.go

package health

import "net/http"

func Read(w http.ResponseWriter, r *http.Request) {}

2. Add api/resource/book/handler.go

package book

import "net/http"

type API struct{}

func (a *API) List(w http.ResponseWriter, r *http.Request) {}

func (a *API) Create(w http.ResponseWriter, r *http.Request) {}

func (a *API) Read(w http.ResponseWriter, r *http.Request) {}

func (a *API) Update(w http.ResponseWriter, r *http.Request) {}

func (a *API) Delete(w http.ResponseWriter, r *http.Request) {}

💡️ In the future, we’ll add the API’s dependencies such as the DB connection, logger, and validator to the API struct.

3. Add Chi router

go get github.com/go-chi/chi/v5

4. Add api/router/router.go

package router

import (
	"github.com/go-chi/chi/v5"

	"myapp/api/resource/book"
	"myapp/api/resource/health"
)

func New() *chi.Mux {
	r := chi.NewRouter()

	r.Get("/livez", health.Read)

	r.Route("/v1", func(r chi.Router) {
		bookAPI := &book.API{}
		r.Get("/books", bookAPI.List)
		r.Post("/books", bookAPI.Create)
		r.Get("/books/{id}", bookAPI.Read)
		r.Put("/books/{id}", bookAPI.Update)
		r.Delete("/books/{id}", bookAPI.Delete)
	})

	return r
}

5. Update cmd/api/main.go

package main

import (
	"fmt"
	"log"
	"net/http"

	"myapp/api/router"
	"myapp/config"
)

func main() {
	c := config.New()
	r := router.New()
	s := &http.Server{
		Addr:         fmt.Sprintf(":%d", c.Server.Port),
		Handler:      r,
		ReadTimeout:  c.Server.TimeoutRead,
		WriteTimeout: c.Server.TimeoutWrite,
		IdleTimeout:  c.Server.TimeoutIdle,
	}

	log.Println("Starting server " + s.Addr)
	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal("Server startup failed")
	}
}

6. Run go mod tidy

When we add a new package and use it, we have to run go mod tidy to reorganize the dependencies in the go.mod file.

💡 You can use the docker-compose down, docker-compose build, and docker-compose up commands, to build and run the API application to test the recent changes. Alternatively, you can configure your IDE to run cmd/api/main.go locally, with the required env variables; Ex: Running applications in the GoLand IDE

Generating OpenAPI specification

The OpenAPI specification is an API description format for REST APIs. It’s a document that shows all API endpoints with their input/ output parameters, authentication methods, etc. So, we need to finalize which information we gather while creating a book or editing it, the format we use to present the book in Read and List APIs, and the format of error cases, like server errors and validation errors.

🔍 swaggo/swag converts Go annotations to the OpenAPI specification. Its GitHub README shows more information about swag CLI options, declarative comments formats, and so on. Also, it uses go structs names in annotations. So, we need to convert finalized input and output formats into Go structs.

We’ll save the data related to the book resource under api/resource/book/model.go but the error structs under api/resource/common/err/err.go, because in the future, the error formats can be used with multiple resource types. Then, we’ll update the cmd/api/main.go to add the general information about the API. After that, we’ll update the health and book resource handlers to add the annotations to each handler method.

1. Finalize input and output formats

| Book Response Json            |  Book create/ update form        |
|                               |                                  |
| (💡 How we present the book   | (💡 The data we gather while     |
| in Read and List APIs)        |  creating a book or editing )    |
|-------------------------------|----------------------------------|
| {                             |   {                              |
|   "id": "string",             |     "title": "string",           |
|   "title": "string",          |     "author": "string",          |
|   "author": "string",         |     "published_date": "string",  |
|   "published_date": "string", |     "image_url": "string",       |
|   "image_url": "string",      |     "description": "string"      |
|   "description": "string"     |   }                              |
| }                             |                                  |

| Error Response Json           |  Errors Response Json            |
|                               |                                  |
| (💡 How we present an error   | (💡 How we present an error with |
| with a single error message)  |  multiple error messages)        |
|-------------------------------|----------------------------------|
| {                             |   {                              |
|   "error": "string"           |     "errors": [                  |
| }                             |          "string",               |
|                               |          "string"                |
|                               |     ]                            |
|                               |   }                              |

2. Add api/resource/book/model.go

package book

type DTO struct {
	ID            string `json:"id"`
	Title         string `json:"title"`
	Author        string `json:"author"`
	PublishedDate string `json:"published_date"`
	ImageURL      string `json:"image_url"`
	Description   string `json:"description"`
}

type Form struct {
	Title         string `json:"title"`
	Author        string `json:"author"`
	PublishedDate string `json:"published_date"`
	ImageURL      string `json:"image_url"`
	Description   string `json:"description"`
}

3. Add api/resource/common/err/err.go

package err

type Error struct {
	Error string `json:"error"`
}

type Errors struct {
	Errors []string `json:"errors"`
}

4. Install swaggo/swag CLI

go install github.com/swaggo/swag/cmd/swag@latest

5. Update cmd/api/main.go

//  @title          MYAPP API
//  @version        1.0
//  @description    This is a sample RESTful API with a CRUD

//  @contact.name   Dumindu Madunuwan
//  @contact.url    https://learning-cloud-native-go.github.io

//  @license.name   MIT License
//  @license.url    https://github.com/learning-cloud-native-go/myapp/blob/master/LICENSE

//  @host       localhost:8080
//  @basePath   /v1
func main() {

6. update api/resource/health/handler.go

// Read godoc
//
//  @summary        Read health
//  @description    Read health
//  @tags           health
//  @success        200
//  @router         /../livez [get]
func Read(w http.ResponseWriter, r *http.Request) {}

7. update api/resource/book/handler.go

// List godoc
//
//  @summary        List books
//  @description    List books
//  @tags           books
//  @accept         json
//  @produce        json
//  @success        200 {array}     DTO
//  @failure        500 {object}    err.Error
//  @router         /books [get]
func (a *API) List(w http.ResponseWriter, r *http.Request) {}
// Create godoc
//
//  @summary        Create book
//  @description    Create book
//  @tags           books
//  @accept         json
//  @produce        json
//  @param          body    body    Form    true    "Book form"
//  @success        201
//  @failure        400 {object}    err.Error
//  @failure        422 {object}    err.Errors
//  @failure        500 {object}    err.Error
//  @router         /books [post]
func (a *API) Create(w http.ResponseWriter, r *http.Request) {}
// Read godoc
//
//  @summary        Read book
//  @description    Read book
//  @tags           books
//  @accept         json
//  @produce        json
//  @param          id	path        string  true    "Book ID"
//  @success        200 {object}    DTO
//  @failure        400 {object}    err.Error
//  @failure        404
//  @failure        500 {object}    err.Error
//  @router         /books/{id} [get]
func (a *API) Read(w http.ResponseWriter, r *http.Request) {}
// Update godoc
//
//  @summary        Update book
//  @description    Update book
//  @tags           books
//  @accept         json
//  @produce        json
//  @param          id      path    string  true    "Book ID"
//  @param          body    body    Form    true    "Book form"
//  @success        200
//  @failure        400 {object}    err.Error
//  @failure        404
//  @failure        422 {object}    err.Errors
//  @failure        500 {object}    err.Error
//  @router         /books/{id} [put]
func (a *API) Update(w http.ResponseWriter, r *http.Request) {}
// Delete godoc
//
//  @summary        Delete book
//  @description    Delete book
//  @tags           books
//  @accept         json
//  @produce        json
//  @param          id  path    string  true    "Book ID"
//  @success        200
//  @failure        400 {object}    err.Error
//  @failure        404
//  @failure        500 {object}    err.Error
//  @router         /books/{id} [delete]
func (a *API) Delete(w http.ResponseWriter, r *http.Request) {}

8. Generate OpenAPI specification

⭐️ swaggo/swag CLI comes with a comment formatter command swag fmt, similar to go fmt, but for swagger comments.

swag init -g cmd/api/main.go -o .swagger -ot yaml command generates the OpenAPI specification in yaml format inside the newly created .swagger folder. You can use -ot json to build it in JSON format. Check swag init -h for more options.

💡Update the .gitignore file to add the .swagger folder, if you don’t want to commit swaggo/swag CLI generated files to the codebase. In the future, we’ll generate the OpenAPI specification and attach it on each release via GitHub actions.

📁 Final project structure

myapp
├── cmd
│  ├── api
│  │  └── main.go
│  └── migrate
│     └── main.go
├── api
│  ├── router
│  │  └── router.go
│  │
│  └── resource
│     ├── health
│     │  └── handler.go
│     ├── book
│     │  ├── handler.go
│     │  └── model.go
│     └── common
│        └── err
│           └── err.go
├── migrations
│  └── 00001_create_books_table.sql
├── config
│  └── config.go
├── .env
├── go.mod
├── go.sum
├── docker-compose.yml
└── Dockerfile

👨‍🏫 What’s next…

In the next article, we’ll add the database repository to our application.