This Banner is For Sale !!
Get your ad here for a week in 20$ only and get upto 15k traffic Daily!!!

Building REST APIs in Golang go-gin with persistence database Postgres


Let’s construct REST APIs in Go utilizing Gin net framework.

I’ll including authentication, docker picture and pattern deployment in Google cloud in subsequent coming articles.

We’re going to construct APIs round Albums that I took from go.dev however I’ll prolong by utilizing the Postgres database. So, this may have persistent information storage.

Often after we are working with net frameworks, they often accommodates all of the required elements to construct full software with out importing another further library. See here

We might be having routing, JSON/XML parsers (let’s imagine well-known information interchange format parsers), validations, sanitizations of enter information, HTTP purchasers to make name to exterior APIs, database drivers, issues that makes response writing straightforward to server, ORMs (object relational mapper), Safety measures corresponding to CSRF, XSS, SQL Injections, logging and so forth.. that are required to construct software simply.

Simply think about, we shouldn’t have totally examined router performance and in case if need to construct router from scratch, it’s actually laborious for regular developer to construct and keep all of the occasions. All of the libraries, frameworks and SDKs present abstraction for us to construct software simply. We solely want to fret about what’s our enterprise logic. In actual fact there are even Go libraries that generated REST APIs skelton for us. Superior proper ?

Sure, it’s, it’s taking place and it’ll proceed to occur that we don’t trouble to write down code for frequent net software objects corresponding to authentication, authorization, enter validations, response era, unit testing, integration testing, logging, middleware, take any performance you wished to have, there might be a number of libraries and APIs (this might be SaaS firms which give enterprise degree issues) which makes issues straightforward for us.

I feel we’re going past our present matter, all I’m sharing right here is fundamental instance of constructing REST APIs which has persistent reminiscence with Postgres Server.

Let’s initialize go module to create to relaxation apis.

go mod init instance.com/web-services

To start with we have to connect with database, I’m maintaining all of the recordsdata in similar major bundle as that is easy one however often we must always divide into packages, there aren’t any official mission structure for Go however often builders comply with issues like handlers, config, providers, apis, area, and so forth like that, however there are like golang project layout

postgressql.sql


CREATE TABLE IF NOT EXISTS artist (
    id SERIAL PRIMARY KEY,
    title VARCHAR ( 50 ) NOT NULL --this is title of the Writer,
    title VARCHAR ( 50 ) UNIQUE NOT NULL,
    value VARCHAR ( 255 ) NOT NULL
);

SELECT id,title,title,value FROM artist LIMIT 5 OFFSET 1;

UPDATE artist 
SET title='nae56',title='tle56', value=66.7667
WHERE id=8;

SELECT * FROM artist;

SELECT * FROM artist WHERE id=8;

DELETE FROM artist WHERE id=1;

INSERT INTO artist(title,title,value) VALUES('name3','title3','price3') ON CONFLICT DO NOTHING;
Enter fullscreen mode

Exit fullscreen mode

database.go


bundle major

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
)

// Env holds database connection to Postgres
sort Env struct {
    DB *sql.DB
}

// database variables
// often we must always get them from env like os.Getenv("variableName")
const (
    host     = "localhost"
    port     = 5439
    consumer     = "postgres"
    password = "root"
    dbname   = "artists"
)

// ConnectDB tries to attach DB and on succcesful it returns
// DB connection string and nil error, in any other case return empty DB and the corresponding error.
func ConnectDB() (*sql.DB, error) {
    connString := fmt.Sprintf("host=%s port=%d consumer=%s password=%s dbname = %s sslmode=disable", host, port, consumer, password, dbname)
    db, err := sql.Open("postgres", connString)
    if err != nil {
        log.Printf("failed to connect with database: %v", err)
        return &sql.DB{}, err
    }
    return db, nil
}
Enter fullscreen mode

Exit fullscreen mode

major.go

bundle major

import (
    "log"

    "github.com/gin-gonic/gin"
)

func major() {
    // connect with DB first
    env := new(Env)
    var err error
    env.DB, err = ConnectDB()
    if err != nil {
        log.Fatalf("failed to begin the server: %v", err)
    }

    router := gin.Default()
    router.GET("/albums/:id", env.GetAlbumByID)
    router.GET("/albums", env.GetAlbums)
    router.POST("/albums", env.PostAlbum)
    router.PUT("/albums", env.UpdateAlbum)
    router.DELETE("/albums/:id", env.DeleteAlbumByID)

    router.Run("localhost:8080")
}

Enter fullscreen mode

Exit fullscreen mode

handler.go

bundle major

// That is to make sure we don't overload the server when we have now thousands and thousands of rows
var recordFetchLimit = 100 // testing objective
Enter fullscreen mode

Exit fullscreen mode

album_get.go

bundle major

import (
    "database/sql"
    "fmt"
    "log"
    "web/http"
    "strconv"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

// GetAlbumByID locates the album whose ID worth matches the id
// parameter despatched by the shopper, then returns that album as a response.
func (env Env) GetAlbumByID(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        e := fmt.Sprintf("obtained invalid id path param which isn't string: %v", c.Param("id"))
        log.Println(e)
        makeGinResponse(c, http.StatusNotFound, e)
        return
    }

    var title, title string
    var value float64
    q := `SELECT * FROM artist WHERE id=$1;`
    row := env.DB.QueryRow(q, id)
    err = row.Scan(&id, &title, &title, &value)
    change err {
    case sql.ErrNoRows:
        log.Printf("no rows are current for alubum with id: %d", id)
        makeGinResponse(c, http.StatusBadRequest, err.Error())
    case nil:
        log.Printf("we're in a position to fetch album with given id: %d", id)
        c.JSON(http.StatusOK, NewAlbum(id, title, title, value))
    default:
        e := fmt.Sprintf("error: %v occurred whereas studying the databse for Album document with id: %d", err, id)
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, err.Error())
    }
}

// GetAlbums responds with the record of all albums as JSON.
func (env Env) GetAlbums(c *gin.Context) {
    // Observe:
    //
    // pagnination could be impleted in could methods, however I'm following one of many approach,
    // in the event you really feel that is complicated, please learn medium article that I've added under
    // For this web page and perPage isseus, entrance finish engineers can care for it
    // by escaping and sanitization of question params.
    // Please see: https://www.enterprisedb.com/postgres-tutorials/how-use-limit-and-offset-postgresql
    // Please see: https://levelup.gitconnected.com/creating-a-data-pagination-function-in-postgresql-2a032084af54
    web page := c.Question("web page") // AKA restrict in SQL phrases
    if web page == "" {
        e := "lacking question param: web page"
        log.Println(e)
        makeGinResponse(c, http.StatusNotFound, e)
        return
    }

    perPage := c.Question("perPage") // AKA restrict in SQL phrases
    if perPage == "" {
        e := "lacking question param: perPage"
        log.Println(e)
        makeGinResponse(c, http.StatusNotFound, e)
        return
    }

    restrict, err := strconv.Atoi(web page)
    if err != nil {
        e := fmt.Sprintf("obtained invalid web page question param which isn't integer : %v", web page)
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
        return
    }

    if restrict > recordFetchLimit {
        // Appears some unhealthy consumer or entrance finish developer enjoying with question params!
        e := fmt.Sprintf("we agreed to fetch lower than %d information however we obtained request for %d", recordFetchLimit, restrict)
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
        return
    }

    offset, err := strconv.Atoi(perPage)
    if err != nil {
        e := fmt.Sprintf("obtained invalid offset question param which isn't integer : %v", web page)
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
        return
    }

    // anyway, let's examine if offset is a destructive worth
    if offset < 0 {
        e := "offset question param can't be destructive"
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
        return
    }

    q := `SELECT id,title,title,value FROM artist LIMIT $1 OFFSET $2;`
    rows, err := env.DB.Question(q, restrict, offset)
    change err {
    case sql.ErrNoRows:
        defer rows.Shut()
        e := "no rows information present in artist desk to learn"
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
    case nil:
        defer rows.Shut()
        a := make([]Album, 0)
        var rowsReadErr bool
        for rows.Subsequent() {
            var id int
            var title, title string
            var value float64
            err = rows.Scan(&id, &title, &title, &value)
            if err != nil {
                log.Printf("error occurred whereas studying the database rows: %v", err)
                rowsReadErr = true
                break
            }
            a = append(a, NewAlbum(id, title, title, value))
        }

        if rowsReadErr {
            log.Println("we aren't in a position to fetch few information")
        }

        // let's return the learn rows a minimum of
        log.Printf("we're in a position to fetch albums for requested restrict: %d and offest: %d", restrict, offset)
        c.JSON(http.StatusOK, a)
    default:
        defer rows.Shut()
        // this could not occur
        e := "some inside database server error"
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
    }
}

Enter fullscreen mode

Exit fullscreen mode

album_post.go

bundle major

import (
    "log"
    "web/http"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

// PostAlbums provides an album from JSON obtained within the request physique.
func (env Env) PostAlbum(c *gin.Context) {
    // Name BindJSON to bind the obtained JSON to
    // newAlbum.
    var newAlbum Album
    if err := c.BindJSON(&newAlbum); err != nil {
        log.Printf("invalid JSON physique: %v", err)
        makeGinResponse(c, http.StatusNotFound, err.Error())
        return
    }

    q := `INSERT INTO artist(title,title,value) VALUES($1,$2,$3) ON CONFLICT DO NOTHING`
    outcome, err := env.DB.Exec(q, newAlbum.Artist, newAlbum.Title, newAlbum.Worth)
    if err != nil {
        log.Printf("error occurred whereas inserting new document into artist desk: %v", err)
        makeGinResponse(c, http.StatusInternalServerError, err.Error())
        return
    }

    // checking the variety of rows affected
    n, err := outcome.RowsAffected()
    if err != nil {
        log.Printf("error occurred whereas checking the returned outcome from database after insertion: %v", err)
        makeGinResponse(c, http.StatusInternalServerError, err.Error())
        return
    }

    // if no document was inserted, allow us to say shopper has failed
    if n == 0 {
        e := "couldn't insert the document, please strive once more after someday"
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
        return
    }

    // NOTE:
    //
    // Right here I wished to return the situation for newly created Album however this
    // 'pq' library doesn't assist, LastInsertionID performance.
    m := "efficiently created the document"
    log.Println(m)
    makeGinResponse(c, http.StatusOK, m)
}

Enter fullscreen mode

Exit fullscreen mode

album_put.go

bundle major

import (
    "fmt"
    "log"
    "web/http"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

// UpdateAlbum updates the Album with the given particulars if document discovered.
func (env Env) UpdateAlbum(c *gin.Context) {
    // Name BindJSON to bind the obtained JSON to
    // toBeUpdatedAlbum.
    var toBeUpdatedAlbum Album
    if err := c.BindJSON(&toBeUpdatedAlbum); err != nil {
        e := fmt.Sprintf("invalid JSON physique: %v", err)
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
        return
    }

    q := `UPDATE artist 
    SET title=$1,title=$2, value=$3
    WHERE id=$4;`
    outcome, err := env.DB.Exec(q, toBeUpdatedAlbum.Artist, toBeUpdatedAlbum.Title, toBeUpdatedAlbum.Worth, toBeUpdatedAlbum.ID)
    if err != nil {
        e := fmt.Sprintf("error: %v occurred whereas updating artist document with id: %d", err, toBeUpdatedAlbum.ID)
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
        return
    }

    // checking the variety of rows affected
    n, err := outcome.RowsAffected()
    if err != nil {
        e := fmt.Sprintf("error occurred whereas checking the returned outcome from database after updation: %v", err)
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
    }

    // if no document was up to date, allow us to say shopper has failed
    if n == 0 {
        e := "couldn't replace the document, please strive once more after someday"
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
        return
    }

    m := "efficiently up to date the document"
    log.Println(m)
    makeGinResponse(c, http.StatusOK, m)
}

Enter fullscreen mode

Exit fullscreen mode

album_delete.go

bundle major

import (
    "fmt"
    "log"
    "web/http"
    "strconv"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

// DeleteAlbumByID locates the album whose ID worth matches the id
// parameter despatched by the shopper, then returns that corresponding message.
func (env Env) DeleteAlbumByID(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        e := fmt.Sprintf("obtained invalid id path param which isn't string: %v", c.Param("id"))
        log.Println(e)
        makeGinResponse(c, http.StatusNotFound, e)
        return
    }

    q := `DELETE FROM artist WHERE id = $1;`
    outcome, err := env.DB.Exec(q, id)
    if err != nil {
        e := fmt.Sprintf("error occurred whereas deleting artist document with id: %d and error is: %v", id, err)
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
        return
    }

    // checking the variety of rows affected
    n, err := outcome.RowsAffected()
    if err != nil {
        e := fmt.Sprintf("error occurred whereas checking the returned outcome from database after deletion: %v", err)
        log.Println(e)
        makeGinResponse(c, http.StatusInternalServerError, e)
        return
    }

    // if no document was deleted, allow us to inform that there is likely to be no
    // information to delete for this given album ID.
    if n == 0 {
        e := "couldn't delete the document, there is likely to be no information for the given ID"
        log.Println(e)
        makeGinResponse(c, http.StatusBadRequest, e)
        return
    }

    m := "efficiently deleted the document"
    log.Println(m)
    makeGinResponse(c, http.StatusOK, m)
}

Enter fullscreen mode

Exit fullscreen mode

We will add enterprise object in area.go

bundle major

// Album holds of few essential particulars about it.
sort Album struct {
    ID     int     `json:"id,omitempty"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Worth  float64 `json:"value"`
}

// NewAlbum is Album constructor.
func NewAlbum(id int, title, artist string, value float64) Album {
    return Album{id, title, artist, value}
}

Enter fullscreen mode

Exit fullscreen mode

There are specific issues in software we repeat once more, could also be we are able to make one operate (DRY principal) add it within the utils.go

bundle major

import (
    "github.com/gin-gonic/gin"
)

func makeGinResponse(c *gin.Context, statusCode int, worth string) {
    c.JSON(statusCode, gin.H{
        "message":    worth,
        "statusCode": statusCode,
    })
}

Enter fullscreen mode

Exit fullscreen mode

In case in order for you attempt to simply take all of the codes and replica it run go mod obtain or go mod tidy to get go.sum and go.mod recordsdata.

I’ve examined them totally, please discover Postman assortment

postmainCollection.json


{
    "data": {
        "_postman_id": "b7f050de-a273-408c-b11d-3ec27fb8c5d4",
        "title": "Learning01",
        "schema": "https://schema.getpostman.com/json/assortment/v2.1.0/assortment.json"
    },
    "merchandise": [
        {
            "name": "Get Albums",
            "request": {
                "method": "GET",
                "header": [],
                "url": {
                    "uncooked": "localhost:8080/albums?web page=100&perPage=78",
                    "host": [
                        "localhost"
                    ],
                    "port": "8080",
                    "path": [
                        "albums"
                    ],
                    "question": [
                        {
                            "key": "page",
                            "value": "100"
                        },
                        {
                            "key": "perPage",
                            "value": "78"
                        }
                    ]
                }
            },
            "response": []
        },
        {
            "title": "Get Albums By ID",
            "request": {
                "technique": "GET",
                "header": [],
                "url": {
                    "uncooked": "localhost:8080/albums/10",
                    "host": [
                        "localhost"
                    ],
                    "port": "8080",
                    "path": [
                        "albums",
                        "10"
                    ]
                }
            },
            "response": []
        },
        {
            "title": "Submit Album",
            "request": {
                "technique": "POST",
                "header": [],
                "physique": {
                    "mode": "uncooked",
                    "uncooked": "{rn    "id":6667778,rn    "title":"newTitle",rn    "artist":"dfjkfdkj",rn    "value":123434.49rn}",
                    "choices": {
                        "uncooked": {
                            "language": "json"
                        }
                    }
                },
                "url": {
                    "uncooked": "localhost:8080/albums",
                    "host": [
                        "localhost"
                    ],
                    "port": "8080",
                    "path": [
                        "albums"
                    ]
                }
            },
            "response": []
        },
        {
            "title": "Replace Album",
            "request": {
                "technique": "PUT",
                "header": [],
                "physique": {
                    "mode": "uncooked",
                    "uncooked": "{rn    "id":10,rn    "title":"dddf",rn    "artist":"88888888",rn    "value":888.88888rn}",
                    "choices": {
                        "uncooked": {
                            "language": "json"
                        }
                    }
                },
                "url": {
                    "uncooked": "localhost:8080/albums",
                    "host": [
                        "localhost"
                    ],
                    "port": "8080",
                    "path": [
                        "albums"
                    ]
                }
            },
            "response": []
        },
        {
            "title": "Delete Album by ID",
            "request": {
                "technique": "DELETE",
                "header": [],
                "url": {
                    "uncooked": "localhost:8080/albums/10",
                    "host": [
                        "localhost"
                    ],
                    "port": "8080",
                    "path": [
                        "albums",
                        "10"
                    ]
                }
            },
            "response": []
        }
    ]
}
Enter fullscreen mode

Exit fullscreen mode

Extra sharing of learnings to return …
Blissful unlearn.be taught,relearn 🙂
Thanks.

The Article was Inspired from tech community site.
Contact us if this is inspired from your article and we will give you credit for it for serving the community.

This Banner is For Sale !!
Get your ad here for a week in 20$ only and get upto 10k Tech related traffic daily !!!

Leave a Reply

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?