├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── drivers └── datastore.go ├── example.config.json ├── handlers ├── errors.go ├── handler.go └── thing_handler.go ├── main.go └── models └── thing.go /.gitignore: -------------------------------------------------------------------------------- 1 | api-starter 2 | api 3 | config.json 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centurylink/ca-certs 2 | WORKDIR /app 3 | COPY api /app/ 4 | COPY config.json /api/config.json 5 | ENTRYPOINT ["./api"] 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ewan Valentine 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD=GOOS=linux ARCH=GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s" -a -installsuffix cgo -o 2 | 3 | default: build 4 | 5 | build: 6 | go get 7 | $(BUILD) ./api . 8 | docker build -t "ewanvalentine:api-starter" . 9 | 10 | run: 11 | docker run -p 5000:5000 ewanvalentine:api-starter 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/EwanValentine/api-starter)](https://goreportcard.com/report/github.com/EwanValentine/api-starter) 2 | 3 | # Golang // Postgres // Echo API Starter 4 | 5 | This is a starter codebase for building API's in Golang. This includes, Postgres for the database driver and echo for the framework. This example also uses fasthttp. 6 | 7 | Please submit pull requests if you spot anything that could be done better. 8 | 9 | A Dockerfile is also included. 10 | 11 | Note: this is mostly for my own convenience, as I write a lot of microservices in Golang. 12 | 13 | ## Building 14 | 15 | You can build this the easy way `go build`, or `go run main.go`. Or you can use `make`, which builds a linux binary, which you can use with the Docker image. 16 | 17 | ## Configure 18 | 19 | You must have a `config.json` file in your project root (see example.config.json). 20 | 21 | ## Run 22 | 23 | Again, you can run it on your host `go run main.go` or `./api-starter` if you've already compiled the binary. Or you can run within Docker `make run` (be sure to have ran `make` first for this). 24 | 25 | ## Use 26 | 1. Create some dummy data `$ curl -i -XPOST --url http://localhost:5000/api/v1/things -d '{ "title": "This is a test", "amount": 12 }' --header "Content-Type: application/json"` 27 | 2. Check it was created `$ curl -i -XGET --url http://localhost:5000/api/v1/things` 28 | -------------------------------------------------------------------------------- /drivers/datastore.go: -------------------------------------------------------------------------------- 1 | package drivers 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | _ "github.com/jinzhu/gorm/dialects/postgres" 6 | ) 7 | 8 | // DB - fetch instance of postgres session 9 | func DB(user, pass, host, name string) *gorm.DB { 10 | 11 | var connection string 12 | 13 | // Connection string 14 | connection = "postgres://" + user + ":" + pass + "@" + host + "/" + name + "?sslmode=disable" 15 | 16 | db, err := gorm.Open("postgres", connection) 17 | 18 | db.LogMode(true) 19 | 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // Ping function checks the database connectivity 25 | err = db.DB().Ping() 26 | 27 | // We have to panic at this stage as api is unusable 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | return db 33 | } 34 | -------------------------------------------------------------------------------- /example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_port": 1234, 3 | "db_host": "localhost", 4 | "db_pass": "postgres", 5 | "db_user": "postgres", 6 | "db_name": "postgres", 7 | "port": "5000" 8 | } 9 | -------------------------------------------------------------------------------- /handlers/errors.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "net/http" 4 | 5 | // Error - Http error 6 | type Error struct { 7 | Message string `json:"_message"` 8 | Code int `json:"_code"` 9 | } 10 | 11 | var ( 12 | // NotFound - Basic 404 response 13 | NotFound = &Error{ 14 | Message: "Resource not found", 15 | Code: http.StatusNotFound, 16 | } 17 | 18 | // Unprocessable - 422 response type 19 | Unprocessable = &Error{ 20 | Message: "Entity unprocessable", 21 | Code: 422, 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /handlers/handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | // Response - Http response 4 | type Response struct { 5 | Data interface{} `json:"data"` 6 | Meta map[string]interface{} `json:"_meta"` 7 | } 8 | -------------------------------------------------------------------------------- /handlers/thing_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/EwanValentine/api-starter/models" 5 | "github.com/labstack/echo" 6 | "net/http" 7 | ) 8 | 9 | // ThingHandler - accepts ThingRepository as arg 10 | type ThingHandler struct { 11 | datastore *models.ThingRepository 12 | } 13 | 14 | // NewThingHandler - Creates new instance of ThingHandler 15 | func NewThingHandler(datastore *models.ThingRepository) *ThingHandler { 16 | return &ThingHandler{ 17 | datastore, 18 | } 19 | } 20 | 21 | // FindAll - Handler to find all the things 22 | func (handler *ThingHandler) FindAll(c echo.Context) error { 23 | 24 | things, err := handler.datastore.FindAll() 25 | 26 | if err != nil { 27 | return c.JSON(http.StatusNotFound, NotFound) 28 | } 29 | 30 | return c.JSON(200, &Response{ 31 | Data: things, 32 | Meta: map[string]interface{}{ 33 | "_link": "/api/v1/things", 34 | }, 35 | }) 36 | } 37 | 38 | // Insert - Handler to insert a thing 39 | func (handler *ThingHandler) Insert(c echo.Context) error { 40 | var thing models.Thing 41 | 42 | c.Bind(&thing) 43 | 44 | err := handler.datastore.Insert(thing) 45 | 46 | if err != nil { 47 | return c.JSON(422, Unprocessable) 48 | } 49 | 50 | return c.JSON(http.StatusCreated, nil) 51 | } 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/EwanValentine/api-starter/drivers" 7 | "github.com/EwanValentine/api-starter/handlers" 8 | "github.com/EwanValentine/api-starter/models" 9 | "github.com/labstack/echo" 10 | "github.com/labstack/echo/engine/fasthttp" 11 | "github.com/labstack/echo/middleware" 12 | "log" 13 | "os" 14 | "runtime" 15 | ) 16 | 17 | // Init - Bootstrap runtime options 18 | func Init() { 19 | 20 | // Verbose logging 21 | log.SetFlags(log.Lshortfile) 22 | 23 | // Use all available cores 24 | runtime.GOMAXPROCS(runtime.NumCPU()) 25 | } 26 | 27 | // Config - Config object 28 | type Config struct { 29 | Port string `json:"port"` 30 | DBHost string `json:"db_host"` 31 | DBPass string `json:"db_pass"` 32 | DBUser string `json:"db_user"` 33 | DBPort int `json:"db_port"` 34 | DBName string `json:"db_name"` 35 | } 36 | 37 | func main() { 38 | 39 | Init() 40 | 41 | var config Config 42 | 43 | // Configure 44 | file, _ := os.Open("./config.json") 45 | decoder := json.NewDecoder(file) 46 | 47 | err := decoder.Decode(&config) 48 | 49 | // Configuration file incorrect or not found 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | datastore := drivers.DB( 55 | config.DBUser, 56 | config.DBPass, 57 | config.DBHost, 58 | config.DBName, 59 | ) 60 | 61 | // Migrate changes 62 | datastore.AutoMigrate(&models.Thing{}) 63 | 64 | e := echo.New() 65 | 66 | // Middleware 67 | e.Use(middleware.Logger()) 68 | e.Use(middleware.Recover()) 69 | 70 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 71 | AllowOrigins: []string{"*"}, 72 | AllowMethods: []string{ 73 | echo.GET, 74 | echo.HEAD, 75 | echo.PUT, 76 | echo.POST, 77 | echo.DELETE, 78 | echo.PATCH, 79 | echo.OPTIONS, 80 | }, 81 | })) 82 | 83 | thingRepository := models.NewThingRepository(datastore) 84 | handlers := handlers.NewThingHandler(thingRepository) 85 | 86 | e.GET("/api/v1/things", handlers.FindAll) 87 | e.POST("/api/v1/things", handlers.Insert) 88 | /* 89 | e.GET("/api/v1/things/:id", handlers.Find) 90 | e.PATCH("/api/v1/things/:id", handlers.Update) 91 | e.DELETE("/api/v1/things/:id", handlers.Remove) 92 | */ 93 | 94 | fmt.Println("Connecting on port " + config.Port) 95 | 96 | e.Run(fasthttp.New(":" + config.Port)) 97 | } 98 | -------------------------------------------------------------------------------- /models/thing.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/satori/go.uuid" 6 | "time" 7 | ) 8 | 9 | // ThingRepository - Repository object for `things` 10 | type ThingRepository struct { 11 | db *gorm.DB 12 | } 13 | 14 | // NewThingRepository - Create a new instance of `ThingRepository` database instance injected 15 | func NewThingRepository(db *gorm.DB) *ThingRepository { 16 | return &ThingRepository{ 17 | db, 18 | } 19 | } 20 | 21 | // Thing - Thing model 22 | type Thing struct { 23 | ID string `gorm:"primary_key:true"` 24 | Title string `json:"title"` 25 | Amount int `json:"amount"` 26 | CreatedAt time.Time 27 | UpdatedAt time.Time 28 | 29 | // This is a pointer so's a nil value can be returned 30 | // Gorm by default checks DeletedAt, so unless nil is 31 | // returned, a blank time will always be checked, 32 | // resulting in 0 results. 33 | DeletedAt *time.Time 34 | } 35 | 36 | // BeforeCreate - Lifecycle callback - Generate UUID before persisting 37 | func (thing *Thing) BeforeCreate(scope *gorm.Scope) error { 38 | scope.SetColumn("ID", uuid.NewV4().String()) 39 | return nil 40 | } 41 | 42 | // FindAll - Find all of the things 43 | func (repository *ThingRepository) FindAll() ([]Thing, error) { 44 | var things []Thing 45 | 46 | err := repository.db.Find(&things).Error 47 | 48 | if err != nil { 49 | return things, err 50 | } 51 | 52 | return things, nil 53 | } 54 | 55 | // Insert - Create a thing 56 | func (repository *ThingRepository) Insert(thing Thing) error { 57 | return repository.db.Create(&thing).Error 58 | } 59 | --------------------------------------------------------------------------------