A Document Driven API to Go
DevDay Caserta
21 April 2017
Luca Corbo
ScientiaMobile
Luca Corbo
ScientiaMobile
We'll go through the life cycle of a simple but fully functional web API:
2
3
API Description Languages are formal languages designed to provide a structured description of a web API that is useful both to a human and for automated machine processing. *
Additional advantages:
4
5
/pet/{petId}:
get:
summary: "Find pet by ID"
description: "Returns a single pet"
operationId: "getPetById"
produces:
- "application/json"
parameters:
- name: "petId"
in: "path"
description: "ID of pet to return"
required: true
type: "integer"
format: "int64"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/Pet"
400:
description: "Invalid ID supplied"
404:
description: "Pet not found"
6
Pet:
type: "object"
required:
- "name"
- "photoUrls"
properties:
id:
type: "integer"
format: "int64"
category:
$ref: "#/definitions/Category"
name:
type: "string"
status:
type: "string"
description: "pet status in the store"
enum:
- "available"
- "pending"
- "sold"
7
8
9
10
11
12
13
Get all tasks
GET /tasks
Add a task
POST /tasks
Get a task
GET /tasks/{id}Edit a task
PUT /tasks/{id}Delete a task
DELETE /tasks/{id}
14
FORMAT: 1A HOST: http://todo.local/api # Another TODO Another TODO API is that, another TODO API.
15
# Group Tasks
Resources related to the tasks in the API.
## Tasks Collection [/tasks]
### List All Tasks [GET]
+ Response 200 (application/json)
[
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"description": "Buy milk",
"completed": false,
},
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"description": "Buy milk",
"completed": false,
},
]
16
## Data Structures ### Task + id: `6ba7b810-9dad-11d1-80b4-00c04fd430c8` (string, required) - Task's ID + description: `Buy milk` (string, required) - Task's description + completed: false (boolean, required) - Done status + createdAt: `2017-04-09T16:30:42+00:00` (string, required) - Task's created date + updatedAt: `2017-04-09T16:30:42+00:00` (string, required) - Task's update date
17
# Group Tasks
Resources related to the tasks in the API.
## Tasks Collection [/tasks]
### List All Tasks [GET]
+ Response 200 (application/json)
+ Attributes (array [Task])
18
## Tasks Collection [/tasks]
...
### Create a New Task [POST]
+ Attributes
+ description: `Buy milk` (string, required) - Task description
+ completed: false (boolean, required) - Done status
+ Request (application/json)
{
"description": "Buy milk",
"completed": false
}
+ Response 201 (application/json)
+ Attributes (Task)
19
### Create a New Task [POST]
+ Attributes
+ description: `Buy milk` (string, required) - Task description
+ completed: false (boolean, required) - Done status
+ Request (application/json)
{
"description": "Buy milk",
"completed": false
}
+ Response 201 (application/json)
+ Attributes (Task)
+ Response 422 (application/json)
{
"error": "Unprocessable entity",
"message": "an error message"
}
20
## Task [/tasks/{taskId}]
+ Parameters
+ taskId: `6ba7b810-9dad-11d1-80b4-00c04fd430c8` (string, required) - ID if the task
### View a Task [GET]
+ Response 200 (application/json)
+ Attributes (Task)
+ Response 404
21
## Task [/tasks/{taskId}]
...
### Edit a Task [PUT]
+ Attributes
+ description: `Buy milk` (string, required) - Task description
+ completed: true (boolean, required) - Done status
+ Request (application/json)
{
"description": "Buy milk",
"completed": true
}
+ Response 200 (application/json)
+ Attributes (Task)
22
## Task [/tasks/{taskId}]
...
### Delete a Task [DELETE]
+ Response 204
23
24
Aglio: an API Blueprint renderer that supports multiple themes and outputs static HTML that can be served by any web host *
Using docker:
docker run --rm -v $PWD:/data lucor/aglio \ -i docs/api-v1.apib \ -o public/docs/index.html \ --theme-full-width
Using the executable:
npm install -g aglio
aglio -i docs/api-v1.apib \
-o public/docs/index.html \
--theme-full-width
25
26
27
Snowboard an API Blueprint toolkit in Go *
docker run -it -v $PWD:/doc -p 8087:8087 bukalapak/snowboard mock -b :8087 docs/api-v1.apib Mock server is ready. Use :8087 Available Routes: GET 200 /tasks POST 201 /tasks POST 422 /tasks GET 200 /tasks/:taskId GET 404 /tasks/:taskId PUT 200 /tasks/:taskId DELETE 204 /tasks/:taskId
28
curl http://127.0.0.1:8087/tasks
[
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"description": "Buy milk",
"completed": false,
"createdAt": "2017-04-09T16:30:42+00:00",
"updatedAt": "2017-04-09T16:30:42+00:00"
}
]
29
curl -v -X POST -H "X-Status-Code: 422" http://127.0.0.1:8087/tasks
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8087 (#0)
> POST /tasks HTTP/1.1
> Host: 127.0.0.1:8087
> User-Agent: curl/7.47.0
> Accept: */*
> X-Status-Code: 422
< HTTP/1.1 422 Unprocessable Entity
< Content-Type: application/json
< Date: Sat, 07 Apr 2018 18:42:05 GMT
< Content-Length: 71
{
"error": "Unprocessable entity",
"message": "an error message"
}
30
31
tree $GOPATH/src/github.com/lucor/document-driven-api-to-go
.
├── api.go
├── api_test.go
├── docs
│ └── api-v1.apib
├── Gopkg.lock
├── Gopkg.toml
├── public
│ └── docs
│ └── index.html
└── vendor
└── ...To test and run our application:
go test -v //runs tests go run api.go //compiles and runs the main package
32
Dep is a tool for managing dependencies for Go projects
Examples:
dep init set up a new project dep ensure install the project's dependencies dep ensure -update update the locked versions of all dependencies dep ensure -add github.com/pkg/errors add a dependency to the project
Dep website: golang.github.io/dep/
Gopher by Ashley McNamara
33
# Gopkg.toml example # # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md # for detailed Gopkg.toml documentation. # [[constraint]] name = "github.com/gorilla/mux" version = "1.6.1" [[constraint]] name = "github.com/satori/go.uuid" version = "1.2.0"
34
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. [[projects]] name = "github.com/gorilla/context" packages = ["."] revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" version = "v1.1" [[projects]] name = "github.com/gorilla/mux" packages = ["."] revision = "53c1911da2b537f792e7cafcb446b05ffe33b996" version = "v1.6.1" [[projects]] name = "github.com/satori/go.uuid" packages = ["."] revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3" version = "v1.2.0"
35
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func main() {
// Create the router
router := mux.NewRouter()
// Serve static files: this will serve our docs under http://localhost:9999/static/docs/
router.PathPrefix("/static/").Handler(
http.StripPrefix("/static/", http.FileServer(http.Dir("public"))),
)
log.Fatal(http.ListenAndServe(":9999", router))
}
36
// storage is a map used to simulate our storage system
var storage map[string]Task
func main() {
// Initialize the storage map
storage = make(map[string]Task)
// Create the router
router := mux.NewRouter()
// Task endpoints
router.HandleFunc("/tasks", TasksHandler).Methods(http.MethodGet)
router.HandleFunc("/tasks", CreateTaskHandler).Methods(http.MethodPost)
router.HandleFunc("/task/{taskID}", GetTaskHandler).Methods(http.MethodGet)
router.HandleFunc("/task/{taskID}", EditTaskHandler).Methods(http.MethodPut)
router.HandleFunc("/task/{taskID}", DeleteTaskHandler).Methods(http.MethodDelete)
...
37
// Task represents a Task
type Task struct {
ID string `json:"id"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// TaskRequestOptions represents the options used to create or edit Task
type TaskRequestOptions struct {
Description string `json:"description"`
Completed bool `json:"completed"`
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Message string `json:"message"`
Error string `json:"error"`
}
38
func TasksHandler(w http.ResponseWriter, r *http.Request) { tasks := []Task{} for _, task := range storage { tasks = append(tasks, task) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) body, _ := json.Marshal(tasks) w.Write(body) }
39
go test -v === RUN TestTasksHandler --- PASS: TestTasksHandler (0.00s) === RUN TestCreateTaskHandler --- PASS: TestCreateTaskHandler (0.00s) === RUN TestGetTaskHandler --- PASS: TestGetTaskHandler (0.00s) === RUN TestEditTaskHandler --- PASS: TestEditTaskHandler (0.00s) === RUN TestDeleteTask --- PASS: TestDeleteTask (0.00s) PASS ok github.com/lucor/document-driven-api-to-go 0.003s
40
import (
"net/http/httptest"
"testing"
)
func TestTasksHandler(t *testing.T) {
// setup the test environment
setup()
// create an HTTP request
req := httptest.NewRequest(http.MethodGet, "/tasks", nil)
// create an HTTP response recorder
rr := httptest.NewRecorder()
// invoke the handler
TasksHandler(rr, req)
// assertions using the response recorder
if status := rr.Code; status != http.StatusOK {
t.Errorf("wrong status code: got %v want %d", status, http.StatusOK)
}
...
41
func setup() { // Initialize the tasks map storage = make(map[string]Task) // Add a fake task now := time.Now().Format(time.RFC3339) storage["6ba7b810-9dad-11d1-80b4-00c04fd430c8"] = Task{ ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", Description: "Buy milk", Completed: false, CreatedAt: now, UpdatedAt: now, } }
42
tasks := []Task{} err := json.NewDecoder(rr.Body).Decode(&tasks) if err != nil { t.Errorf("unable to decode body: got %s", rr.Body.String()) } if len(tasks) != 1 { t.Errorf("unexpected len: got %d want %d", len(tasks), 1) } task := tasks[0] if task.ID != "6ba7b810-9dad-11d1-80b4-00c04fd430c8" { t.Errorf("unexpected ID: got %v want %v", task.ID, "6ba7b810-9dad-11d1-80b4-00c04fd430c8") }
43
func CreateTaskHandler(w http.ResponseWriter, r *http.Request) { // Parse and validate request options := TaskRequestOptions{} err := json.NewDecoder(r.Body).Decode(&options) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) body, _ := json.Marshal(ErrorResponse{ Message: "unable to parse the body request", Error: err.Error(), }) w.Write(body) return }
44
// Create the Task now := time.Now().Format(time.RFC3339) task := Task{ ID: uuid.NewV4().String(), Description: options.Description, Completed: options.Completed, CreatedAt: now, UpdatedAt: now, } // Persist the Task storage[task.ID] = task // Return the response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) body, _ := json.Marshal(task) w.Write(body) }
45
func TestCreateTaskHandler(t *testing.T) { setup() req := httptest.NewRequest(http.MethodPost, "/tasks", strings.NewReader(` { "description": "Buy bread", "completed": false } `)) rr := httptest.NewRecorder() CreateTaskHandler(rr, req)
46
if status := rr.Code; status != http.StatusOK { t.Errorf("wrong status code: got %v want %d", status, http.StatusOK) } tasks := []Task{} err := json.NewDecoder(rr.Body).Decode(&tasks) if err != nil { t.Errorf("unable to decode body: got %s", rr.Body.String()) } if len(tasks) != 1 { t.Errorf("unexpected len: got %d want %d", len(tasks), 1) } task := tasks[0] if task.ID != "6ba7b810-9dad-11d1-80b4-00c04fd430c8" { t.Errorf("unexpected ID: got %v want %v", task.ID, "6ba7b810-9dad-11d1-80b4-00c04fd430c8") } }
47
func DeleteTaskHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) taskID := vars["taskID"] delete(storage, taskID) w.WriteHeader(http.StatusNoContent) }
48
func TestDeleteTaskHandler(t *testing.T) { setup() req := httptest.NewRequest(http.MethodDelete, "/tasks/{taskId}", nil) // sets the URL variables for the given request, to be accessed via mux.Vars for testing route behaviour. req = mux.SetURLVars(req, map[string]string{"taskID": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"}) rr := httptest.NewRecorder() DeleteTaskHandler(rr, req) if status := rr.Code; status != http.StatusNoContent { t.Errorf("wrong status code: got %v want %d", status, http.StatusNoContent) } }
49
Luca Corbo
ScientiaMobile