DEV Community

loading...
Cover image for RestFull API Detailing

RestFull API Detailing

AdrnlnJnky
I am a Veteran outdoor leadership mentor and wilderness guide with a coding addiction. Looking to be a full time coder.
・9 min read

Shaping UP

In the last few posts I build a CRUD interface, set up some testing and connected to a database. Great now I can store three strings of data. I know things are labelled with the word contacts but really we are just storing three strings. You could store anything you want in here right now. That doesn't seem right. I need to give this thing some constraints.

A quick review of my main.go file:


package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"

    "gitlab.com/adrnlnjnky/RestAPI/contacts"
)

func main() {
    port := "23.73.19.2:5000"
    r := mux.NewRouter()

    r.HandleFunc("/contacts", contacts.GetContacts).Methods("GET")
    r.HandleFunc("/contacts/{search}", contacts.GetContact).Methods("GET")
    r.HandleFunc("/contacts", contacts.CreateContact).Methods("POST")
    r.HandleFunc("/contacts/{search}", contacts.UpdateContact).Methods("PUT")
    r.HandleFunc("/contacts/{search}", contacts.UpdateContact).Methods("PATCH")
    r.HandleFunc("/contacts/{search}", contacts.DeleteContact).Methods("DELETE")

    if err := http.ListenAndServe(port, r); err != nil {
        log.Fatalf("could not listen on port %v. Error: %v", port, err)
    }
}

Enter fullscreen mode Exit fullscreen mode

This is a pretty simple server set up using gorilla/mux and HandleFunc to direct the json string where I want it to go. So far so good, now lets look at the top of my contacts.go file:


package contacts

import (
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
    "gitlab.com/adrnlnjnky/RestAPI/checker"
    "gitlab.com/adrnlnjnky/RestAPI/dbase"
    "gorm.io/gorm"
)

Enter fullscreen mode Exit fullscreen mode

As you can see from my imports I'm using GORM to control the database which is opened by a package located in /dbase. I'm passing the information around with json and the /checker module is were we are going to qualify our entries before
we insert them; you know, make sure a phone number looks like a phone number. Here is my Contact and Contacts types and a and a few helper functions:


// Contact is the struct for the contacts list.
type Contact struct {
    gorm.Model
    Name string `json:"name"  gorm:"index"`
    Phone string `json:"phone" gorm:"uniqueIndex"`
    Email string `json:"email" gorm:"uniqueIndex"`
}

// Contacts is a slice of type Contact.
type Contacts []Contact

// sendJSON takes in and sends data back in JSON form with the Header set.
func sendJSON(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

// This calls out to open the database then makes the link with the table.
func setupDB(r *http.Request) *gorm.DB {
    query := r.URL.Query()
    database := query.Get("database")
    db := dbase.GormOpenDB(database)
    db.AutoMigrate(&Contact{})
    return db
}

func dbError(w http.ResponseWriter, err error) {
    errNote := fmt.Sprintf("Something went wrong, got this error: %+v\n", err)
    w.WriteHeader(http.StatusBadGateway)
    sendJSON(w, errNote)
}

func noRowsError(w http.ResponseWriter, results string) {
    w.WriteHeader(http.StatusNotFound)
    note := fmt.Sprintf("Was not able to find your request: %v", results)
    sendJSON(w, note)
}
Enter fullscreen mode Exit fullscreen mode

I've labels my Contact fields so that json and gorm both know exactly what I am talking about. The helpers should be self explanatory, one send json and the other one sets up a database to use. My first function is GetContacts which simple returns all the contacts.


func GetContacts(w http.ResponseWriter, r *http.Request) {
    contact := Contacts{}
    db := setupDB(r)

    err := db.Model(&Contact{}).Find(&contact)

    if err.Error != nil {
        dbError(w, err.Error)
    return
    }

    if err.RowsAffected < 1 {
        w.WriteHeader(http.StatusNoContent)
        note := fmt.Sprint(":Nobody found: Have you created anybody in your contact list?")
        sendJSON(w, note)
        return
    }

    sendJSON(w, contact)
}

Enter fullscreen mode Exit fullscreen mode

Not much new here other than constructing a more elaborate message to return if there are no contacts and handling any errors that might come up. Ask for all the contacts you get all the contacts. This seems OK, lets move on and look at
the GetContact function.


func GetContact(w http.ResponseWriter, r *http.Request) {

    par := mux.Vars(r)
    name := par["name"]       // extract the name wanted

    contact := Contacts       // create a variable to put the info in.

    db := openDB(w, r)        // open the database

    results := db.Model(&Contact{}).Where("name = ?", name).First(&contact)

    if err.Error != nil {
        dbError(w, err.Error)
        return
    }
    if results.RowsAffected != 1 {             // Check we found someone
      noRowsError(w, "")
      return
    }

    w.WriteHeader(http.StatusFound)        //respond with desired info
    sendJSON(w, contact)
  }

Enter fullscreen mode Exit fullscreen mode

Well that does get one contact but it only searches by name, and what if I don't remember exactly how I named my contact? Lets see if we can beef this function
up a bit...


func GetContact(w http.ResponseWriter, r *http.Request) {
    par := mux.Vars(r)
    search := par["search"]

    column, regsearch := checker.SortSearch(search)    // This Sorts name | phone | email

target := fmt.Sprintf("%v LIKE ?", column)  

    db := setupDB(r)       // grab the database
    contact := Contacts{}  // create a slice to send back

             // make the call
err := db.Debug().Model(&Contact{}).Where(target, regsearch).Find(&contact)

if err.Error != nil {         // deal with errors
    dbError(w, err.Error)
    return
}
if results.RowsAffected != 1 {    // deal with no results
    noRowsError(w, "")
    return
    }

   // Send back all contacts matching the request.
   sendJSON(w, contact)
}

Enter fullscreen mode Exit fullscreen mode

Again I handled any errors that might come up and added some text to the returning messages. I also added some regex to the postgres call so that I can return on partial names. In addition I build a helper function that will look at the incoming request and tell postgres which column to look in. Wanna see my sorter?


package checker

import (
    "net"
    "regexp"
    "strings"
)

var (
    emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

    phoneRegex = regexp.MustCompile(`^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$`)
)

Enter fullscreen mode Exit fullscreen mode

OK so I don't speak regex as well as I should but I found these variables here:

https://www.golangprograms.com/regular-expressions.html


func SortSearch(data string) string {

    isEmail := IsEmail(data)    
                             // if it's an email return that
    if isEmail == true {
        parts := strings.Split(data, "@")        
                             // split on the @
        reg = "%" + parts[0] + "%" + parts[1]    
                            // send only the first half back
        return "email", reg                      
                            // with a little regex
    }

    isPhone := isPhone(data)     
                           // if it's a phone return that
    if isPhone == true {
        return "phone", reg
    }
    return "name", reg                                                 
                        // for anything else send back name
}

func isPhone(data string) bool {            
                              // searching the phone column

    if len(data) < 3 || len(data) > 25 {      
                              // at least 3 numbers
        return false                            
                        // otherwise might as well search all
    }
    if !phoneRegex.MatchString(data) {        
                                   // do the regex check
        return false
    }
    return true                               
                      // if it passes all my tests send true
}


Enter fullscreen mode Exit fullscreen mode

All that seems easy enough and now we can search on all three of the columns in the database, and phone numbers and email addresses will be accepted. I also split out the email and I'm only searching on the handle note the domain portion. I think this will allow for ar least some searching potential. At this point I felt kind of dirty because my function names were drifings. I decided to rename my get functions and add one more. I renamed:

  • GetContacts -> GetAllContacts
  • GetContact to GetContacts

Actually I copied GetContatcts and renamed it then I made a one change to GetContact. I changed the GORM call from .Find to .Last. Now GetContact will give back exactly one contact while GetContacts will give back all contacts meeting the search. In my main function I made the adjustments and then I pointed /contact/{search} to GetContact.


    go r.HandleFunc("/contacts/{search}", contacts.GetContact).Methods("GET")
    go r.HandleFunc("/contacts/{search}", contacts.GetContacts).Methods("GET")
    go r.HandleFunc("/contacts", contacts.GetAllContacts).Methods("GET")

Enter fullscreen mode Exit fullscreen mode

While I was working this out I was also adding tests to my test file. Test files can get long and messy, basically I added test to cover the phone searching, the email searching and the partial information searches. Like I said things get long and repetitive so lets move on to the CreateContacts.


func CreateContact(w http.ResponseWriter, r *http.Request){
db := setupDB(r)

var contact Contacts                                     
                                   // where to store data
_ = json.NewDecoder(r.Body).Decode(&contact)            
                                   // store the data sent

checkPhone := checker.IsPhoneValid(contact.Phone)     
                                    // qualify phone
checkEmail := checker.IsEmailValid(contact.Email)     
                                    // qualify email

switch {    This switch kicks back if the 
            email and phone do  not evaluate true

case checkPhone == false:
  w.WriteHeader(http.StatusNotAcceptable)
  response := fmt.Sprintf("Something seems wrong with the provided phone number. | %v\n", contact.Phone)
   sendJSON(w, response)

case checkEmail == false:
  w.WriteHeader(http.StatusNotAcceptable)
  response := fmt.Sprintf("Something seems wrong with the provided email address. | %v\n", contact.Email)
  sendJSON(w, response)
    }

results := db.Model(&Contact{}).Create(&contact)   
                      // record the new contact

switch {              // handle the errors or not recorded

case err.Error != nil:
   dbError(w, err.Error)  
case results.RowsAffected != 1:     
             // I'm not sure this code will ever run but hey

default:                // send back the all good
  newcontact := fmt.Sprintf("New contact created %v\n", &r.Body)
  sendJSON(w, newcontact)
    }
}

Enter fullscreen mode Exit fullscreen mode

We already looked at my sorting helper, when it comes to recording a new number or email I have a bit stricter requirements when it comes to recording. So I build these function in my checker module.


func IsPhoneValid(data string) bool {

    if len(data) < 10 || len(data) > 20 {  // here I want at least 7 numbers
        return false 
    }
    if !phoneRegex.MatchString(data) {    
                                       // do the regex check
        return false
    }
    return true                // if you pass my tests send true
}




func IsEmailValid(search string) bool {

if len(search) < 5 && len(search) > 54 {     
                                          // Length limiter
  return false
  }

if !emailRegex.MatchString(search) {        
                                // match that string I found
  return false
  }

parts := strings.Split(search, "@")         
                                // split on the @

mx, err := net.LookupMX(parts[1])           
                             // is this from an email server?

if err != nil || len(mx) == 0 {             
                             // if an error return false
  return false            // if mx has no length return false
  }
  return true             // if you pass my tests send true
}

Enter fullscreen mode Exit fullscreen mode

I don't ever make international calls but some folks do I expect, so I left some wiggle room for the numbers to take a few shapes. I'm also checking to make sure that the email is valid instead of just stripping it down for a search.

IsEmailValid checks if the email provided passes the required structure and length test. It also checks the domain has a valid MX record.

From the functions descritpon:

// LookupMX returns the DNS MX records for the given domain name sorted by preference.

So more or less I'm getting email address and phone numbers, I'm not doing anything on names because this is a contacts page and you can name things whatever you want.

DeleteContact like GetContacts doesn't need so much work but there are a few errors to handle and some flowery language to be added.


func DeleteContact(w http.ResponseWriter, r *http.Request) {
    db := setupDB(r)
    par := mux.Vars(r)
    search := par["search"]
    column, _ := checker.SortSearch(search)
    target := fmt.Sprintf("%v = ?", column)
    err := db.Where(target, search).Delete(&Contact{})
  if err.Error != nil {
    dbError(w, err.Error)
  }
      if err.RowsAffected != 1 {
    noRowsError(w, search)
    }
    w.WriteHeader(http.StatusGone)
    note := fmt.Sprintf("Deleted contact with %v: %v", column, search)
    sendJSON(w, note)
}

Enter fullscreen mode Exit fullscreen mode

Update like Create contact is going to use the email and phone validators to qualify any entry changes. I also changed the way that I read into the database. Lets look at our new and longer update.


func UpdateContact(w http.ResponseWriter, r *http.Request) {
    db := setupDB(r)

    par := mux.Vars(r)
    search := par["search"]

    column, _ := checker.SortSearch(search)  // throwing away regex modification

    var contact Contact                    // read info into contact
    _ = json.NewDecoder(r.Body).Decode(&contact)

    checkPhone := checker.IsPhoneValid(contact.Phone)
    if checkPhone == false {
        w.WriteHeader(http.StatusNotAcceptable)
        response := fmt.Sprintf("Not a valid phone number. | %v\n", contact.Phone)
        sendJSON(w, response)
    }

    checkEmail := checker.IsEmailValid(contact.Email)
    if checkEmail == false {
        w.WriteHeader(http.StatusNotAcceptable)
        response := fmt.Sprintf("Not a valid email address. | %v\n", contact.Email)
        sendJSON(w, response)
    }

    target := fmt.Sprintf("%v = ?", column)

    results := db.Model(&Contact{}).Where(target, search).Updates(&contact)

    switch {
    case err.Error != nil:
    dbError(w, err.Error)
    case results.RowsAffected != 1:
    noRowsError(w, "search")
    default:
        fixedcontact := fmt.Sprintf("contact %v Updated to:\n name : %v\n  phone: %v\n email : %v\n",
            search,
            contact.Name,
            contact.Phone,
            contact.Email,
        )

        w.WriteHeader(http.StatusAccepted)
        sendJSON(w, fixedcontact)             // send back the update
    }
}

Enter fullscreen mode Exit fullscreen mode

I feel a little bit better about all of this now. Of coarse I might want some other fields for my contacts. That should be pretty easy to do now that we have these three build up. So what else do you want to know about your contacts? Lets make a list and sort the list into searchable and non-searchable columns.

Searchable | NonSearchable


birthday | notes
alt. phone | spouce
work address | children
work phone
work
last name
first name
nick name
alt email 1
alt email 2
alt email 3
twitter
instagram
facebook?
linkdin
github
gitlab
twitch
youtube
zoom -> it that a thing?

I'm sure there are a other things that could be configured too. The other thing that I can do before I start making the contacts all specialized and featured is copy it off and reuse it. It can easily be used for users, employees, members, and with not to much work products, services and whatever else I might want. I mean for users I can just delete the phone number and presto. Then I can build
out a few features and some copy and paste magic presto employee it born. I think I will build up a few of these things and just stick them on a shelf, but not today. Well it's getting kind of late and my brain kinda hurts so until
next time, be well and remember to smile cause yup it might hurt.

Discussion (0)