title | slug |
---|---|
Routes and OpenAPI specification |
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
andgo-chi/chi
are the popular router packages in the Go ecosystem. We'll go withgo-chi/chi
because of its lightweightness and 100% compatibility withnet/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 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
, andDelete
method names to implementCRUD
. We useRead
instead ofGet
here to avoid confusion with theGET
HTTP method onList
andRead
. Also, if you want to support pagination or need to get the total/ filtered items count, you can align with theList
,Count
,Create
,Read
,Update
, andDelete
method names.
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.
package health
import "net/http"
func Read(w http.ResponseWriter, r *http.Request) {}
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
.
go get github.com/go-chi/chi/v5
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
}
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")
}
}
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
, anddocker-compose up
commands, to build and run the API application to test the recent changes. Alternatively, you can configure your IDE to runcmd/api/main.go
locally, with the required env variables; Ex: Running applications in the GoLand IDE
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.
| 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" |
| | ] |
| | } |
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"`
}
package err
type Error struct {
Error string `json:"error"`
}
type Errors struct {
Errors []string `json:"errors"`
}
go install github.com/swaggo/swag/cmd/swag@latest
// @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() {
// Read godoc
//
// @summary Read health
// @description Read health
// @tags health
// @success 200
// @router /../livez [get]
func Read(w http.ResponseWriter, r *http.Request) {}
// 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) {}
βοΈ 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 commitswaggo/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.
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
In the next article, weβll add the database repository to our application.