👨🏫 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 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
, 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.
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
, 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
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 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.
📁 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.