DEV Community

Cover image for Rest API Database Blog Post
AdrnlnJnky
AdrnlnJnky

Posted on

Rest API Database Blog Post

In my last two articles I built a Restful api with a simple CRUD interface operating on contacts and set up some basic tests for it. Now that I can interact with my contact list it would be nice if I could keep that data someplace other than in the computers memory. For this task I'm going to reach for Postgres. I chose Postgres because it's the database I've been learning the last couple of months.
In addition to Postgres I've been trying to learn the GORM interface. GORM is an ORM for Go that works with Postgres and is supposed to make working with SQL easier. I have to share with you that I have spent more time smoking from the
ears trying to get GORM to do something that I had already implemented with raw SQL and the standard Go library than I spent writing the SQL statements in the first place. Persistence is an important part of learning so I'm going to keep beating my head against the GORM project a bit longer.

GORM does make some things very simple, like setting up the database. Automigrate(&SomeStruct) will set up/ align a table in your database with a struct in your code. GORM also works well with embedded tables. It comes with a nice embedded table gorm.Model which gives back ID, CreatedAT, UpdatedAt,
DeletedAt. (Yes DeletedAt means that there is a soft delete function, nice for those over zealous button pushers.) I'm going to use both of these features right away. Now when it comes to the Automigrate function I'm not really sure what the best practice is but I'm going to use it pretty prolificly here to align my Contact struct with my Postgres Table. I should not here that you can name your tables anything you want but GORM has naming convention that I followed. Here is the top bits of my contacts.go file.


package contacts

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

    "github.com/gorilla/mux"
    "gorm.io/gorm"
)

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

// Contacts is a slice of Contact.
var Contacts []Contact
Enter fullscreen mode Exit fullscreen mode

GORM likes the use of tags just like JSON so one of the things I'm gong to explore today is how I want to build those tags. One option is a generic tag something like "name", the other is more explicit json:"name" gorm:"name". In this case with both using name the generic probably works
fine, in fact most of the time it's probably just fine.

now the question: Do I alter the existing functions to hit the database or do I make a call to a new function? The delete function will get shorter if we just reach out to the database but do we need to worry about time? Do I want to pull the database into memory? Probably not and I don't really like the delete function I have, on the other hand I want to thing about making functions as generic as possible to help with a growing program.

The other question that I have to solve is how to handle the database. In the past I have just passed it around. I'm not sure how best to handle this problem here. I can set the params to be "database"="projectDB". Now I know the name of the database I'm working in but I'm not opening the database. What is the best way to pass around the database? For now I'm going to build a helper function to open the database for me. Before a helper function will be helpful I need to open the database. In a folder called dbase I wrote opener.go.


package dbase

import (
    "log"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func GormOpenDB(dbase string) *gorm.DB {
    dsn := "user=postgres dbname=devdatabase port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Failed to open database: %v", db)
    }
    return db
}

Enter fullscreen mode Exit fullscreen mode

Easy enough just tell it what database you want to open and it will send you back a pointer you can use. Now the helper function.


func openDB(w http.ResponseWriter, r *http.Request) *gorm.DB {
    query := r.URL.Query()
    database := query.Get("database")
    db := dbase.GormOpenDB(database)
    db.AutoMigrate(&Contact{})
    return db
}

Enter fullscreen mode Exit fullscreen mode

I just pass the hole thing over grab the Params get the desired database, open it link to my table and send the database back. (I should note here that I added a query to my tests so that they would set the params properly.) This should help keep my code clean and easy to read. Oh while I was at it I decided to pull the json encoding bit out:


func sendJSON(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

Enter fullscreen mode Exit fullscreen mode

I know it's only two lines but every little bit helps. Ok now I can query my database. I'm going to start with create because there isn't anything in the contacts table yet.


func CreateContact(w http.ResponseWriter, r *http.Request) {
    var contact Contact
    _ = json.NewDecoder(r.Body).Decode(&contact)
    db := openDB(w, r)
    db.Model(&Contact{}).Create(&contact)
    sendJSON(w, contact)
}

Enter fullscreen mode Exit fullscreen mode

Pretty simple read the info create the new contact and away we go. I ran that a few times and made a couple instances of my text contact. In the future I may restrict multiple instances and I'll most likely do that with a database restriction. For now I'm going to let it be and move on to getting all contacts.


func GetContacts(w http.ResponseWriter, r *http.Request) {
    contact := Contacts
    db := openDB(w, r)
    db.Model(&Contact{}).Find(&contact)
    sendJSON(w, contact)
}

Enter fullscreen mode Exit fullscreen mode

Again here like create I call out to the dbOpen to create my database instance, then I query the database for all the users and send it back with my json helper. Getting one contact is similar and GORM has a few commands to do this depending on your needs. I'm just going to ask for the first instance.


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 results.RowsAffected != 1 {             // Check we found someone
        w.WriteHeader(http.StatusNotFound)      // respond if we did not
        sendJSON(w, par)
    }

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

Enter fullscreen mode Exit fullscreen mode

I can make them and I can find them now lets delete them.


func DeleteContact(w http.ResponseWriter, r *http.Request) {
    par := mux.Vars(r)
    name := par["name"]
    db := openDB(w, r)
    results := db.Where("name = ?", name).Delete(&Contact{})
    if results.RowsAffected != 1 {
        w.WriteHeader(http.StatusNotFound)
        sendJSON(w, par)
    }
    w.WriteHeader(http.StatusGone)
    sendJSON(w, "Deleted")
}

Enter fullscreen mode Exit fullscreen mode

Everything here should look pretty familiar the only new bit here is the Delete
move. Now to update update!


`func UpdateContact(w http.ResponseWriter, r *http.Request) {
    par := mux.Vars(r)
    name := par["name"]                   // still finding our name the same way

    var contact map[string]interface{}    // a place to store the new information

    blob, _ := ioutil.ReadAll(r.Body)
    err := json.Unmarshal(blob, &contact)  // had to change our reader here.
    if err != nil {
        panic(err)                          // I promise to handle the err
    }
    for k, v := range contact {           // range the info sent in
        db := openDB(w, r)                  // open database and save the stuff
        db.Model(&Contact{}).Where("name = ?", name).Updates(map[string]interface{}{k: v})

        w.WriteHeader(http.StatusAccepted)  // write Status to header
        sendJSON(w, contact)                // send it back
    }
}

Enter fullscreen mode Exit fullscreen mode

I had to change things up a bit here. The reason I did things this way was to handle cases where I get only part of the new data. This way if I get all new information or only the updated information the function can handle it. This
will update everyone with the same name but that's a problem for my next post.

This program is up and running with a simple interface a database to keep our contacts in and a bit of testing. I'd like to note that the only change I made to the test was to add the database information into the params. Other than that I just leaned on my tests to make sure my code was still working. So, With the code covered by some testing I'm
going to build out my contact info and tighten up some restrictions. I mean it would be nice if phone numbers looked like phone numbers and I didn't duplicate contacts a bunch. That said Thank you for reading and have a great day!

Tom Peltier
Smile it might hurt!

Top comments (0)