GoCRUD
Hub
goCRUD

API REST Server

REST API-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)
}