├── .gitattributes ├── README.md ├── main.go └── static └── home.tpl /.gitattributes: -------------------------------------------------------------------------------- 1 | static/* linguist-vendored 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Read full article here](https://thedevsaddam.github.io/post/lets-make-a-simple-todo-app-with-go/) 2 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-chi/chi" 14 | "github.com/go-chi/chi/middleware" 15 | "github.com/thedevsaddam/renderer" 16 | mgo "gopkg.in/mgo.v2" 17 | "gopkg.in/mgo.v2/bson" 18 | ) 19 | 20 | var rnd *renderer.Render 21 | var db *mgo.Database 22 | 23 | const ( 24 | hostName string = "localhost:27017" 25 | dbName string = "demo_todo" 26 | collectionName string = "todo" 27 | port string = ":9000" 28 | ) 29 | 30 | type ( 31 | todoModel struct { 32 | ID bson.ObjectId `bson:"_id,omitempty"` 33 | Title string `bson:"title"` 34 | Completed bool `bson:"completed"` 35 | CreatedAt time.Time `bson:"createAt"` 36 | } 37 | 38 | todo struct { 39 | ID string `json:"id"` 40 | Title string `json:"title"` 41 | Completed bool `json:"completed"` 42 | CreatedAt time.Time `json:"created_at"` 43 | } 44 | ) 45 | 46 | func init() { 47 | rnd = renderer.New() 48 | sess, err := mgo.Dial(hostName) 49 | checkErr(err) 50 | sess.SetMode(mgo.Monotonic, true) 51 | db = sess.DB(dbName) 52 | } 53 | 54 | func homeHandler(w http.ResponseWriter, r *http.Request) { 55 | err := rnd.Template(w, http.StatusOK, []string{"static/home.tpl"}, nil) 56 | checkErr(err) 57 | } 58 | 59 | func createTodo(w http.ResponseWriter, r *http.Request) { 60 | var t todo 61 | 62 | if err := json.NewDecoder(r.Body).Decode(&t); err != nil { 63 | rnd.JSON(w, http.StatusProcessing, err) 64 | return 65 | } 66 | 67 | // simple validation 68 | if t.Title == "" { 69 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 70 | "message": "The title field is requried", 71 | }) 72 | return 73 | } 74 | 75 | // if input is okay, create a todo 76 | tm := todoModel{ 77 | ID: bson.NewObjectId(), 78 | Title: t.Title, 79 | Completed: false, 80 | CreatedAt: time.Now(), 81 | } 82 | if err := db.C(collectionName).Insert(&tm); err != nil { 83 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 84 | "message": "Failed to save todo", 85 | "error": err, 86 | }) 87 | return 88 | } 89 | 90 | rnd.JSON(w, http.StatusCreated, renderer.M{ 91 | "message": "Todo created successfully", 92 | "todo_id": tm.ID.Hex(), 93 | }) 94 | } 95 | 96 | func updateTodo(w http.ResponseWriter, r *http.Request) { 97 | id := strings.TrimSpace(chi.URLParam(r, "id")) 98 | 99 | if !bson.IsObjectIdHex(id) { 100 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 101 | "message": "The id is invalid", 102 | }) 103 | return 104 | } 105 | 106 | var t todo 107 | 108 | if err := json.NewDecoder(r.Body).Decode(&t); err != nil { 109 | rnd.JSON(w, http.StatusProcessing, err) 110 | return 111 | } 112 | 113 | // simple validation 114 | if t.Title == "" { 115 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 116 | "message": "The title field is requried", 117 | }) 118 | return 119 | } 120 | 121 | // if input is okay, update a todo 122 | if err := db.C(collectionName). 123 | Update( 124 | bson.M{"_id": bson.ObjectIdHex(id)}, 125 | bson.M{"title": t.Title, "completed": t.Completed}, 126 | ); err != nil { 127 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 128 | "message": "Failed to update todo", 129 | "error": err, 130 | }) 131 | return 132 | } 133 | 134 | rnd.JSON(w, http.StatusOK, renderer.M{ 135 | "message": "Todo updated successfully", 136 | }) 137 | } 138 | 139 | func fetchTodos(w http.ResponseWriter, r *http.Request) { 140 | todos := []todoModel{} 141 | 142 | if err := db.C(collectionName). 143 | Find(bson.M{}). 144 | All(&todos); err != nil { 145 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 146 | "message": "Failed to fetch todo", 147 | "error": err, 148 | }) 149 | return 150 | } 151 | 152 | todoList := []todo{} 153 | for _, t := range todos { 154 | todoList = append(todoList, todo{ 155 | ID: t.ID.Hex(), 156 | Title: t.Title, 157 | Completed: t.Completed, 158 | CreatedAt: t.CreatedAt, 159 | }) 160 | } 161 | 162 | rnd.JSON(w, http.StatusOK, renderer.M{ 163 | "data": todoList, 164 | }) 165 | } 166 | 167 | func deleteTodo(w http.ResponseWriter, r *http.Request) { 168 | id := strings.TrimSpace(chi.URLParam(r, "id")) 169 | 170 | if !bson.IsObjectIdHex(id) { 171 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 172 | "message": "The id is invalid", 173 | }) 174 | return 175 | } 176 | 177 | if err := db.C(collectionName).RemoveId(bson.ObjectIdHex(id)); err != nil { 178 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 179 | "message": "Failed to delete todo", 180 | "error": err, 181 | }) 182 | return 183 | } 184 | 185 | rnd.JSON(w, http.StatusOK, renderer.M{ 186 | "message": "Todo deleted successfully", 187 | }) 188 | } 189 | 190 | func main() { 191 | stopChan := make(chan os.Signal) 192 | signal.Notify(stopChan, os.Interrupt) 193 | 194 | r := chi.NewRouter() 195 | r.Use(middleware.Logger) 196 | r.Get("/", homeHandler) 197 | 198 | r.Mount("/todo", todoHandlers()) 199 | 200 | srv := &http.Server{ 201 | Addr: port, 202 | Handler: r, 203 | ReadTimeout: 60 * time.Second, 204 | WriteTimeout: 60 * time.Second, 205 | IdleTimeout: 60 * time.Second, 206 | } 207 | 208 | go func() { 209 | log.Println("Listening on port ", port) 210 | if err := srv.ListenAndServe(); err != nil { 211 | log.Printf("listen: %s\n", err) 212 | } 213 | }() 214 | 215 | <-stopChan 216 | log.Println("Shutting down server...") 217 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 218 | srv.Shutdown(ctx) 219 | defer cancel() 220 | log.Println("Server gracefully stopped!") 221 | } 222 | 223 | func todoHandlers() http.Handler { 224 | rg := chi.NewRouter() 225 | rg.Group(func(r chi.Router) { 226 | r.Get("/", fetchTodos) 227 | r.Post("/", createTodo) 228 | r.Put("/{id}", updateTodo) 229 | r.Delete("/{id}", deleteTodo) 230 | }) 231 | return rg 232 | } 233 | 234 | func checkErr(err error) { 235 | if err != nil { 236 | log.Fatal(err) //respond with error page or message 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /static/home.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |