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