API Server

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/module/mode/value

Where module normally corresponds to a SQL table, but also can refer to a function with many joined tables. The mode part is normally what you want to do. And last one or several values. Sent in url or in body.

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