├── .gitignore ├── LICENSE ├── Procfile ├── README.md └── src └── todo-backend ├── main.go ├── middleware.go ├── model.go └── repository.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go 2 | 3 | ### Go ### 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michael Forman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: todo-backend 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # todo-backend-golang 2 | A backend for TodoMVC implemented with Go using no external dependencies 3 | -------------------------------------------------------------------------------- /src/todo-backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var TodoSvc *MockTodoService 13 | 14 | func main() { 15 | port := os.Getenv("PORT") 16 | 17 | if port == "" { 18 | log.Fatal("$PORT must be set") 19 | } 20 | 21 | TodoSvc = NewMockTodoService() 22 | mux := http.NewServeMux() 23 | 24 | mux.Handle("/todos", commonHandlers(todoHandler)) 25 | mux.Handle("/todos/", commonHandlers(todoHandler)) 26 | 27 | log.Fatal(http.ListenAndServe(":"+port, mux)) 28 | } 29 | 30 | func addUrlToTodos(r *http.Request, todos ...*Todo) { 31 | scheme := "http" 32 | if r.TLS != nil { 33 | scheme = "https" 34 | } 35 | baseUrl := scheme + "://" + r.Host + "/todos/" 36 | 37 | for _, todo := range todos { 38 | todo.Url = baseUrl + strconv.Itoa(todo.Id) 39 | } 40 | } 41 | 42 | func todoHandler(w http.ResponseWriter, r *http.Request) { 43 | parts := strings.Split(r.URL.Path, "/") 44 | key := "" 45 | if len(parts) > 2 { 46 | key = parts[2] 47 | } 48 | 49 | switch r.Method { 50 | case "GET": 51 | if len(key) == 0 { 52 | todos, err := TodoSvc.GetAll() 53 | if err != nil { 54 | http.Error(w, err.Error(), http.StatusInternalServerError) 55 | return 56 | } 57 | addUrlToTodos(r, todos...) 58 | json.NewEncoder(w).Encode(todos) 59 | } else { 60 | id, err := strconv.Atoi(key) 61 | if err != nil { 62 | http.Error(w, "Invalid Id", http.StatusBadRequest) 63 | return 64 | } 65 | todo, err := TodoSvc.Get(id) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | if todo == nil { 71 | http.NotFound(w, r) 72 | return 73 | } 74 | addUrlToTodos(r, todo) 75 | json.NewEncoder(w).Encode(todo) 76 | } 77 | case "POST": 78 | if len(key) > 0 { 79 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 80 | return 81 | } 82 | 83 | todo := Todo{ 84 | Completed: false, 85 | } 86 | err := json.NewDecoder(r.Body).Decode(&todo) 87 | if err != nil { 88 | http.Error(w, err.Error(), 422) 89 | return 90 | } 91 | err = TodoSvc.Save(&todo) 92 | if err != nil { 93 | http.Error(w, err.Error(), http.StatusInternalServerError) 94 | return 95 | } 96 | addUrlToTodos(r, &todo) 97 | w.WriteHeader(http.StatusCreated) 98 | json.NewEncoder(w).Encode(todo) 99 | case "PATCH": 100 | id, err := strconv.Atoi(key) 101 | if err != nil { 102 | http.Error(w, "Invalid Id", http.StatusBadRequest) 103 | return 104 | } 105 | var todo Todo 106 | err = json.NewDecoder(r.Body).Decode(&todo) 107 | if err != nil { 108 | http.Error(w, err.Error(), 422) 109 | return 110 | } 111 | todo.Id = id 112 | 113 | err = TodoSvc.Save(&todo) 114 | if err != nil { 115 | if strings.ToLower(err.Error()) == "not found" { 116 | http.NotFound(w, r) 117 | return 118 | } 119 | http.Error(w, err.Error(), http.StatusInternalServerError) 120 | return 121 | } 122 | addUrlToTodos(r, &todo) 123 | json.NewEncoder(w).Encode(todo) 124 | case "DELETE": 125 | if len(key) == 0 { 126 | TodoSvc.DeleteAll() 127 | } else { 128 | id, err := strconv.Atoi(key) 129 | if err != nil { 130 | http.Error(w, "Invalid Id", http.StatusBadRequest) 131 | return 132 | } 133 | err = TodoSvc.Delete(id) 134 | if err != nil { 135 | http.Error(w, err.Error(), http.StatusInternalServerError) 136 | return 137 | } 138 | } 139 | w.WriteHeader(http.StatusNoContent) 140 | default: 141 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 142 | return 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/todo-backend/middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func cors(next http.Handler) http.Handler { 8 | fn := func(w http.ResponseWriter, r *http.Request) { 9 | w.Header().Set("access-control-allow-origin", "*") 10 | w.Header().Set("access-control-allow-methods", "GET, POST, PATCH, DELETE") 11 | w.Header().Set("access-control-allow-headers", "accept, content-type") 12 | if r.Method == "OPTIONS" { 13 | return // Preflight sets headers and we're done 14 | } 15 | next.ServeHTTP(w, r) 16 | } 17 | 18 | return http.HandlerFunc(fn) 19 | } 20 | 21 | func contentTypeJsonHandler(next http.Handler) http.Handler { 22 | fn := func(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 24 | next.ServeHTTP(w, r) 25 | } 26 | 27 | return http.HandlerFunc(fn) 28 | } 29 | 30 | func commonHandlers(next http.HandlerFunc) http.Handler { 31 | return contentTypeJsonHandler(cors(next)) 32 | } 33 | -------------------------------------------------------------------------------- /src/todo-backend/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import () 4 | 5 | type Todo struct { 6 | Id int `json:"-"` 7 | Title string `json:"title"` 8 | Completed bool `json:"completed"` 9 | Order int `json:"order"` 10 | Url string `json:"url"` 11 | } 12 | -------------------------------------------------------------------------------- /src/todo-backend/repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Define an interface for the data methods to support different storage types 9 | type TodoService interface { 10 | GetAll() ([]Todo, error) 11 | Get(id int) (*Todo, error) 12 | Save(todo *Todo) error 13 | DeleteAll() error 14 | Delete(id int) error 15 | } 16 | 17 | // MockTodoService uses a concurrent array for basic testing 18 | type MockTodoService struct { 19 | m sync.Mutex 20 | nextId int 21 | Todos []*Todo 22 | } 23 | 24 | func NewMockTodoService() *MockTodoService { 25 | t := new(MockTodoService) 26 | t.m.Lock() 27 | t.Todos = make([]*Todo, 0) 28 | t.nextId = 1 // Start at 1 so we can distinguish from unspecified (0) 29 | t.m.Unlock() 30 | return t 31 | } 32 | 33 | func (t *MockTodoService) GetAll() ([]*Todo, error) { 34 | return t.Todos, nil 35 | } 36 | 37 | func (t *MockTodoService) Get(id int) (*Todo, error) { 38 | for _, value := range t.Todos { 39 | if value.Id == id { 40 | return value, nil 41 | } 42 | } 43 | return nil, nil 44 | } 45 | 46 | func (t *MockTodoService) Save(todo *Todo) error { 47 | if todo.Id == 0 { // Insert 48 | t.m.Lock() 49 | todo.Id = t.nextId 50 | t.nextId++ 51 | t.m.Unlock() 52 | 53 | t.m.Lock() 54 | t.Todos = append(t.Todos, todo) 55 | t.m.Unlock() 56 | return nil 57 | } 58 | 59 | // Update existing 60 | for i, value := range t.Todos { 61 | if value.Id == todo.Id { 62 | t.Todos[i] = todo 63 | return nil 64 | } 65 | } 66 | 67 | return fmt.Errorf("Not Found") 68 | } 69 | 70 | func (t *MockTodoService) DeleteAll() error { 71 | t.m.Lock() 72 | t.Todos = make([]*Todo, 0) 73 | t.m.Unlock() 74 | return nil 75 | } 76 | 77 | func (t *MockTodoService) Delete(id int) error { 78 | for i, value := range t.Todos { 79 | if value.Id == id { 80 | t.m.Lock() 81 | t.Todos = append(t.Todos[:i], t.Todos[i+1:]...) 82 | t.m.Unlock() 83 | return nil 84 | } 85 | } 86 | return nil 87 | } 88 | --------------------------------------------------------------------------------