├── .gitignore ├── Dockerfile ├── create-binary.sh ├── benchmarks ├── todo.json └── post.sh ├── main.go ├── models └── task.go ├── docker-compose.yml ├── README.md ├── Gopkg.toml ├── controllers └── tasks.go ├── db └── db.go └── Gopkg.lock /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD go-binary / 3 | CMD ["/go-binary"] 4 | -------------------------------------------------------------------------------- /create-binary.sh: -------------------------------------------------------------------------------- 1 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-binary . 2 | -------------------------------------------------------------------------------- /benchmarks/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "test", 3 | "created_at": "2017-11-13T23:03:28-08:00", 4 | "completed": false 5 | } 6 | -------------------------------------------------------------------------------- /benchmarks/post.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "POST http://localhost:8080/api/v1/tasks/" | vegeta attack -body todo.json -duration=30s -rate=1000 | tee results-POST.bin | vegeta report 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "github.com/gin-gonic/gin" 6 | "github.com/hugomd/go-todo/db" 7 | TaskController "github.com/hugomd/go-todo/controllers" 8 | ) 9 | 10 | func main() { 11 | log.Println("Starting server..") 12 | 13 | db.Init(); 14 | 15 | r := gin.Default() 16 | 17 | v1 := r.Group("/api/v1") 18 | { 19 | tasks := v1.Group("/tasks") 20 | { 21 | tasks.GET("/", TaskController.GetTasks) 22 | tasks.POST("/", TaskController.CreateTask) 23 | tasks.PUT("/:id", TaskController.UpdateTask) 24 | tasks.DELETE("/:id", TaskController.DeleteTask) 25 | } 26 | } 27 | 28 | r.Run() 29 | } 30 | -------------------------------------------------------------------------------- /models/task.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | "github.com/satori/go.uuid" 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | // Task "Object 10 | type Task struct { 11 | ID uuid.UUID `json:"id"` 12 | Title string `json:"title" binding:"required"` 13 | CreatedAt time.Time `json:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at"` 15 | Completed bool `json:"completed"` 16 | } 17 | 18 | func (task *Task) BeforeCreate(scope *gorm.Scope) error { 19 | scope.SetColumn("CreatedAt", time.Now()) 20 | scope.SetColumn("ID", uuid.NewV4().String()) 21 | return nil 22 | } 23 | 24 | func (task *Task) BeforeUpdate(scope *gorm.Scope) error { 25 | scope.SetColumn("UpdatedAt", time.Now()) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | todo: 5 | image: go-todo:latest 6 | # build: . 7 | restart: always 8 | depends_on: 9 | - postgres 10 | links: 11 | - postgres 12 | environment: 13 | - PORT=8080 14 | - PG_USER=postgres 15 | - PG_PASSWORD=secret 16 | - PG_HOST=postgres 17 | - PG_PORT=5432 18 | - PG_DB=tasks 19 | ports: 20 | - 8080:8080 21 | postgres: 22 | image: postgres:10.1-alpine 23 | restart: always 24 | healthcheck: 25 | test: ["CMD-SHELL", "pg_isready -U postgres"] 26 | interval: 30s 27 | timeout: 30s 28 | retries: 3 29 | environment: 30 | - POSTGRES_PASSWORD=secret 31 | - POSTGRES_USER=postgres 32 | - POSTGRES_DB=tasks 33 | ports: 34 | - 5432:5432 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-todo 2 | > Simple to do list API with Gin and Gorm (with Postgres) 3 | 4 | ## Docker 5 | Clone this repository and run: 6 | ``` 7 | docker-compose up 8 | ``` 9 | 10 | You can then hit the following endpoints: 11 | 12 | | Method | Route | Body | 13 | | ------ | ---------- | -------------------------------------------- | 14 | | GET | /tasks | | 15 | | POST | /tasks | `{"title": "task title"}` | 16 | | DELETE | /tasks/:id | | 17 | | PUT | /tasks/:id | `{"title": "task title", "completed": true}` | 18 | 19 | ## Development 20 | ### Installing 21 | I'm using [`dep`](https://github.com/golang/dep): 22 | ```bash 23 | dep ensure 24 | ``` 25 | ### Running locally 26 | ``` 27 | go run main.go 28 | ``` 29 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/gin-gonic/gin" 26 | version = "1.2.0" 27 | 28 | [[constraint]] 29 | name = "github.com/jinzhu/gorm" 30 | version = "1.0.0" 31 | 32 | [[constraint]] 33 | branch = "master" 34 | name = "github.com/lib/pq" 35 | 36 | [[constraint]] 37 | name = "github.com/satori/go.uuid" 38 | version = "1.1.0" 39 | -------------------------------------------------------------------------------- /controllers/tasks.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "net/http" 5 | "github.com/gin-gonic/gin" 6 | // "github.com/gin-gonic/gin/binding" 7 | "github.com/hugomd/go-todo/models" 8 | "github.com/hugomd/go-todo/db" 9 | ) 10 | 11 | func GetTasks(c *gin.Context) { 12 | 13 | var tasks []models.Task 14 | db := db.GetDB() 15 | db.Find(&tasks) 16 | c.JSON(200, tasks) 17 | } 18 | 19 | func CreateTask(c *gin.Context) { 20 | var task models.Task 21 | var db = db.GetDB() 22 | 23 | if err := c.BindJSON(&task); err != nil { 24 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ 25 | "error": err.Error(), 26 | }) 27 | return 28 | } 29 | db.Create(&task) 30 | c.JSON(http.StatusOK, &task) 31 | } 32 | 33 | func UpdateTask(c *gin.Context) { 34 | id := c.Param("id") 35 | var task models.Task 36 | 37 | db := db.GetDB() 38 | if err := db.Where("id = ?", id).First(&task).Error; err != nil { 39 | c.AbortWithStatus(http.StatusNotFound) 40 | return 41 | } 42 | c.BindJSON(&task) 43 | db.Save(&task) 44 | c.JSON(http.StatusOK, &task) 45 | } 46 | 47 | func DeleteTask(c *gin.Context) { 48 | id := c.Param("id") 49 | var task models.Task 50 | db := db.GetDB() 51 | 52 | if err := db.Where("id = ?", id).First(&task).Error; err != nil { 53 | c.AbortWithStatus(http.StatusNotFound) 54 | return 55 | } 56 | 57 | db.Delete(&task) 58 | } 59 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "log" 7 | _ "github.com/lib/pq" 8 | "github.com/jinzhu/gorm" 9 | _ "github.com/jinzhu/gorm/dialects/postgres" 10 | "github.com/hugomd/go-todo/models" 11 | ) 12 | 13 | var db *gorm.DB 14 | var err error 15 | 16 | func getEnv(key, fallback string) string { 17 | if value, ok := os.LookupEnv(key); ok { 18 | return value 19 | } 20 | return fallback 21 | } 22 | 23 | // Init creates a connection to mysql database and 24 | // migrates any new models 25 | func Init() { 26 | user := getEnv("PG_USER", "hugo") 27 | password := getEnv("PG_PASSWORD", "") 28 | host := getEnv("PG_HOST", "localhost") 29 | port := getEnv("PG_PORT", "8080") 30 | database := getEnv("PG_DB", "tasks") 31 | 32 | dbinfo := fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", 33 | user, 34 | password, 35 | host, 36 | port, 37 | database, 38 | ) 39 | 40 | db, err = gorm.Open("postgres", dbinfo) 41 | if err != nil { 42 | log.Println("Failed to connect to database") 43 | panic(err) 44 | } 45 | log.Println("Database connected") 46 | 47 | if !db.HasTable(&models.Task{}) { 48 | err := db.CreateTable(&models.Task{}) 49 | if err != nil { 50 | log.Println("Table already exists") 51 | } 52 | } 53 | 54 | db.AutoMigrate(&models.Task{}) 55 | } 56 | 57 | //GetDB ... 58 | func GetDB() *gorm.DB { 59 | return db 60 | } 61 | 62 | func CloseDB() { 63 | db.Close() 64 | } 65 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/gin-contrib/sse" 7 | packages = ["."] 8 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 9 | 10 | [[projects]] 11 | name = "github.com/gin-gonic/gin" 12 | packages = [".","binding","render"] 13 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" 14 | version = "v1.2" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/golang/protobuf" 19 | packages = ["proto"] 20 | revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" 21 | 22 | [[projects]] 23 | name = "github.com/jinzhu/gorm" 24 | packages = [".","dialects/postgres"] 25 | revision = "5174cc5c242a728b435ea2be8a2f7f998e15429b" 26 | version = "v1.0" 27 | 28 | [[projects]] 29 | branch = "master" 30 | name = "github.com/jinzhu/inflection" 31 | packages = ["."] 32 | revision = "1c35d901db3da928c72a72d8458480cc9ade058f" 33 | 34 | [[projects]] 35 | branch = "master" 36 | name = "github.com/lib/pq" 37 | packages = [".","hstore","oid"] 38 | revision = "8c6ee72f3e6bcb1542298dd5f76cb74af9742cec" 39 | 40 | [[projects]] 41 | name = "github.com/mattn/go-isatty" 42 | packages = ["."] 43 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 44 | version = "v0.0.3" 45 | 46 | [[projects]] 47 | name = "github.com/satori/go.uuid" 48 | packages = ["."] 49 | revision = "879c5887cd475cd7864858769793b2ceb0d44feb" 50 | version = "v1.1.0" 51 | 52 | [[projects]] 53 | branch = "master" 54 | name = "github.com/ugorji/go" 55 | packages = ["codec"] 56 | revision = "8338af5bac521965300f901d1817d1fd20ee0d9e" 57 | 58 | [[projects]] 59 | branch = "master" 60 | name = "golang.org/x/sys" 61 | packages = ["unix"] 62 | revision = "bf42f188b9bc6f2cf5b8ee5a912ef1aedd0eba4c" 63 | 64 | [[projects]] 65 | name = "gopkg.in/go-playground/validator.v8" 66 | packages = ["."] 67 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 68 | version = "v8.18.2" 69 | 70 | [[projects]] 71 | branch = "v2" 72 | name = "gopkg.in/yaml.v2" 73 | packages = ["."] 74 | revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f" 75 | 76 | [solve-meta] 77 | analyzer-name = "dep" 78 | analyzer-version = 1 79 | inputs-digest = "d487388bb69744ba18b3252e5cb2fe80e98223f8ae8145fef5db75e80f7a7c52" 80 | solver-name = "gps-cdcl" 81 | solver-version = 1 82 | --------------------------------------------------------------------------------