API REST Server
There seems to be no common ground for creating a REST API. Using Go is no exception. My goal is to create a maintainable, fast and simple REST Api.
In my attempt to create an API, I use a lookup table for every SQL query rather than hard coding every query into Go. My thought is that this will reduce the number of endpoints in the API. As a bonus you can manage queries on-the-fly without compiling every time.
When developing this API there are 6 Go files. Compiled into ONE single 8MB binary. No other files or dependencies. In my dreams this simple API will manage almost all simple queries. In reality I think this will cover almost half of all needed queries. This API has no CORS limitations. This will be added in production.
URL Naming
I decided to do my own naming 3 part principle:
https://api.go4webdev.org/scope/action/value
Where scope normally corresponds to a SQL table, but also can refer to a function with many joined tables. The action part is normally what you want to do. And last one or several values
The stored SQL query is a combination of scope and action. So /user/id/1 is stored as user_id in the SQL lookup database.
Here is the Go files for the API:
1. The main.go file (4 endpoints and a SQL lookup query)
package main import ( //"fmt" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "net/http" "os" "strings" ) var db *sqlx.DB func main() { Connect() http.HandleFunc("/", handler) http.Handle("/favicon.ico", http.NotFoundHandler()) http.ListenAndServe(":9998", nil) } func handler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT,CREATE,DELETE") w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Content-Type", "application/json") switch r.Method { case "DELETE": Delete(w, r) case "POST": Create(w, r) case "PUT": Update(w, r) default: //GET Get(w, r) } } func Getquery(path string) string { // get query from lookup db var query string err := db.QueryRow("SELECT sql_query FROM sqls WHERE sql_id=$1", path).Scan(&query) if err != nil { path = "" } return query } func getpath(r *http.Request) (string, string, string) { path := strings.Split(r.URL.String(), "/") switch len(path) { case 4: return path[1], path[2], path[3] case 3: return path[1], path[2], "" case 2: return path[1], "", "" default: return "", "", "" } } // log store at main level in file log.txt on server func log(msg string) { file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY, 0644) if err != nil { log(err.Error()) return } defer file.Close() _, err2 := file.WriteString(msg + "\n") if err2 != nil { log(err.Error()) return } }
2. The connect.go file (Connect to Postgresql)
In this example I use the same database for both lookup and data.
package main import ( "fmt" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) func Connect() { const ( host = "94.237.90.200" port = 5432 user = "postgres" password = "password" dbname = "lookup" ) login := fmt.Sprintf("host=%s port=%d user=%s "+ "password=%s dbname=%s sslmode=require", host, port, user, password, dbname) var err error db, err = sqlx.Connect("postgres", login) if err != nil { log(err.Error()) } err = db.Ping() if err != nil { log(err.Error()) } }
3. The create.go file (Subrouter for CREATE)
package main import ( "encoding/json" "io/ioutil" "net/http" "net/url" ) func Create(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { log(err.Error()) } scope, action, val := getpath(r) val = string(body) var data interface{} val, _ = url.QueryUnescape(val) switch action { case "new": query := Getquery(scope + "_" + action) data = new(query, val) } json.NewEncoder(w).Encode(data) } // add new record func new(query string, val string) interface{} { var id int err := db.QueryRowx(query, val).Scan(&id) if err != nil { return nil } return (id) }
4. The read.go file (Subrouter for READ)
package main import ( "encoding/json" //"fmt" "net/http" "net/url" ) func Read(w http.ResponseWriter, r *http.Request) { scope, action, val := getpath(r) var data interface{} val, _ = url.QueryUnescape(val) switch action { case "id": query := Getquery(scope + "_" + action) data = getid(query, val) case "all": query := Getquery(scope + "_" + action) data = getall(query) } // convert to json and write json.NewEncoder(w).Encode(data) } // return a single row func getid(query string, val string) interface{} { if len(query) > 0 { row := make(map[string]interface{}) db.QueryRowx(query, val).MapScan(row) return (row) } return nil } // query to return a list func getall(query string) interface{} { if len(query) > 0 { var list []map[string]interface{} rows, err := db.Queryx(query) if err != nil { log("no records") } defer rows.Close() for rows.Next() { row := make(map[string]interface{}) err = rows.MapScan(row) if err != nil { log(err.Error()) } list = append(list, row) } rows.Close() if len(list) == 0 { return ("norec") } return list } return nil }
5. The update.go file (Subrouter for UPDATE)
package main import ( "io/ioutil" "net/http" "net/url" ) func Update(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { log(err.Error()) } scope, action, val := getpath(r) val = string(body) val, _ = url.QueryUnescape(val) switch action { case "edit": query := Getquery(scope + "_" + action) edit(query, val) } } // edit record func edit(query string, val string) { db.MustExec(query, val) }
6. The delete.go file (Subrouter for DELETE)
package main import ( //"fmt" "net/http" ) func Delete(w http.ResponseWriter, r *http.Request) { scope, action, val := getpath(r) switch action { case "del": query := Getquery(scope + "_" + action) delete(query, val) } } // delete record func delete(query string, val string) { db.MustExec(query, val) }