Souvik Haldar
27 Aug 2019
•
8 min read
REST, acronym for Representational State Transfer, is the architectural approach to building web services. Let's discuss a few characteristics of REST:
/addCustomer
is invalid whereas /customer
with POST verb is better suited because 'customer' is a noun which is the resource here.(we'll discuss HTTP verbs later) /customer/24
signifies the customer with a unique identifier 24
. POST
: Create a new resource (non-idempotent in nature)PUT
: Update a resource or collection of resources (idempotent in nature)GET
: Retrieve a resource or collection of resources.DELETE
: Delete a resource or collection of resources. In this guide, we will create a simple API which can perform CRUD operations (i.e Create Read Update Delete ) on the 'customer' resource.
C --> create a new customer instance (POST)
R --> fetch a customer's details (GET)
U --> update a customer's detail (PUT)
D --> delete a customer's instance (DELETE)
The standard library of Go is really very powerful, you can write a full blown server by using just the standard library!
I’m assuming your system is setup for writing Go programs, if not you can follow this link and get it done.
Create a directory in the $GOPATH/src/<vcs-name>/<username>/golang-server
or you can name it anything you want. I’m naming in golang-server
.
The above path is standard golang practice, but after the onset of go modules
you can create the directory anywhere you want.
For me the path looks like:-
/Users/souvik/Development/go/src/github.com/souvikhaldar/golang-server
Create a file called main.go
(you can name it whatever) at the root. This is where the main
function resides.
We can store data anywhere, but for persistent storage a database is always a good option. In this guide, we will be using PostgreSQL.
So let's setup the database connection now.
(i) Install postgresql on your system. Follow this link
(ii) Run psql -U postgres
on the terminal. (NOTE: postgres
is a default role automatically created, if it's not you need to create it. Also, my commands are for mac, but other OSs should be pretty similar)
(iii) Create a new database. create database guide
, I'm naming it guide, you can name it anything.
(iv) Create a simple table with two columns customer_id
and customer_name
by running create table customer(customer_id int,customer_name text);
after connecting the guide
database (do \c guide
)
(v) We need a third party library for better database handling. Use govendor for dependency management hence install it this way:-
(a) At the project root- govendor init
(b) govendor fetch github.com/lib/pq
(Note: If we would have not used govendor for dependency management we could have installed using- go get -u github.com/lib/pq
but using one is always a better idea)
(vi) init
method in golang is the method that runs first even before running main
hence we will setup the database connection there.
func init() {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%sdbname=%s sslmode=disable", host, port, user, dbname)
fmt.Println("conection string: ", psqlInfo)
var err error
dbDriver, err = sql.Open("postgres", psqlInfo)
if err != nil {
panic(err)
}
err = dbDriver.Ping()
if err != nil {
panic(err)
}
fmt.Println("Successfully connected to postgres!")
}
First of all, let's register all the handlers that would be performing the CRUD
operations. We are using a very efficient third-party router called "gorilla mux".
router := mux.NewRouter()
router.HandleFunc("/customer", addCustomer).Methods("POST")
router.HandleFunc("/customer/{id}", updateCustomer).Methods("PUT")
router.HandleFunc("/customer/{id}", deleteCustomer).Methods("DELETE")
router.HandleFunc("/customer", fetchCustomers).Methods("GET")
log.Fatal(http.ListenAndServe(":8192", router))
We've provided 8192
as the port on which the server should run run, but in case that port is already used by some other service, choose any other port, like 8193
,8188
,etc.
POST
Add a new resource
Now let's write the handler for each of the operations.
Below is the handler for adding a new customer's details which will essentially insert a new instance of customer to our database.
func addCustomer(w http.ResponseWriter, r *http.Request) {
var requestbody customer
if err := json.NewDecoder(r.Body).Decode(&requestbody); err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Println("Data recieved: ", requestbody)
if _, err := dbDriver.Exec("INSERT INTO customer(customer_id,customer_name) VALUES($1,$2)", requestbody.CustomerID, requestbody.CustomerName); err != nil {
fmt.Println("Error in inserting to the database")
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintln(w, "Successfully inserted: ", requestbody)
}
What it is doing is, first it is reading the request which contains a JSON data as body and unmarshalling it into customer
struct, then it is making INSERT
query to the database to add the data.
Sample request:-
curl --location --request POST "localhost:8192/customer" \
--header "Content-Type: application/json" \
--data "{
\"CustomerID\":4,
\"CustomerName\": \"souvik\"
}"
PUT
The code for updating the user is as follows:-
func updateCustomer(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r)
idS := v["id"]
id, _ := strconv.Atoi(idS)
fmt.Println("Updating customer: ", id)
var requestbody customerUpdate
if err := json.NewDecoder(r.Body).Decode(&requestbody); err != nil {
http.Error(w, err.Error(), 500)
return
}
if _, err := dbDriver.Exec("UPDATE customer set customer_name=$1 where customer_id=$2", requestbody.CustomerName, id); err != nil {
fmt.Println("Error in updating: ", err)
http.Error(w, err.Error(), 500)
}
fmt.Fprintln(w, "Succesfully updated user")
}
We pass the data to be updated in the body
of the request and customer ID of the customer whose details are being updated is passed in the URL.
(NOTE: later when you see the request you will understand it better, for now focus on the logic.)
Sample request:-
curl --location --request PUT "localhost:8192/customer/4" \
--header "Content-Type: application/json" \
--data "{
\"CustomerName\":\"haldar\"
}"
DELETE
Let's try to DELETE
a resource now.
func deleteCustomer(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r)
id := v["id"]
fmt.Println("Deleting user: ", id)
if _, err := dbDriver.Exec("DELETE FROM customer where customer_id=$1", id); err != nil {
fmt.Println("Unable to delete the customer: ", err)
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintln(w, "Successfully deleted!")
}
In the above code, we are passing the ID of the customer to be deleted from our records. The query for deletion the simple DELETE
command.
Sample request:-
curl --location --request DELETE "localhost:8192/customer/4"
GET
Now finally, let's try to fetch the details of all customer data in JSON format.
func fetchCustomers(w http.ResponseWriter, r *http.Request) {
fmt.Println("Fetching all customers")
rows, err := dbDriver.Query("SELECT * from customer")
if err != nil {
fmt.Println("Unable to read the table: ", err)
http.Error(w, err.Error(), 500)
return
}
var customers []customer
defer rows.Close()
for rows.Next() {
var c customer
if err := rows.Scan(&c.CustomerID, &c.CustomerName); err != nil {
fmt.Println("Unable to scan")
}
customers = append(customers, c)
}
customerJSON, err := json.Marshal(customers)
if err != nil {
fmt.Println("Unable to marshall the data: ", err)
http.Error(w, err.Error(), 500)
return
}
fmt.Println("Customers: ", customers)
fmt.Fprintln(w, string(customerJSON))
}
In the above code, we are querying for all the customer records, accessing them one by one and appending to a slice and finally serializing them into JSON using the Marshall
method.
Sample request:-
curl --location --request GET "localhost:8192/customer"
The final code looks like:-
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
)
const (
host = "localhost"
port = 5432
user = "postgres"
dbname = "guide"
)
var dbDriver *sql.DB
func init() {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s dbname=%s sslmode=disable", host, port, user, dbname)
var err error
dbDriver, err = sql.Open("postgres", psqlInfo)
if err != nil {
panic(err)
}
err = dbDriver.Ping()
if err != nil {
panic(err)
}
fmt.Println("Successfully connected to postgres!")
}
type customer struct {
CustomerID int32
CustomerName string
}
type customerUpdate struct {
CustomerName string
}
// Adding a new customer to the database
func addCustomer(w http.ResponseWriter, r *http.Request) {
fmt.Println("Adding a new customer")
var requestbody customer
if err := json.NewDecoder(r.Body).Decode(&requestbody); err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Println("Data recieved: ", requestbody)
if _, err := dbDriver.Exec("INSERT INTO customer(customer_id,customer_name) VALUES($1,$2)", requestbody.CustomerID, requestbody.CustomerName); err != nil {
fmt.Println("Error in inserting to the database")
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintln(w, "Successfully inserted: ", requestbody)
}
// Update the details of a customer
func updateCustomer(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r)
idS := v["id"]
id, _ := strconv.Atoi(idS)
fmt.Println("Updating customer: ", id)
var requestbody customerUpdate
if err := json.NewDecoder(r.Body).Decode(&requestbody); err != nil {
http.Error(w, err.Error(), 500)
return
}
if _, err := dbDriver.Exec("UPDATE customer set customer_name=$1 where customer_id=$2", requestbody.CustomerName, id); err != nil {
fmt.Println("Error in updating: ", err)
http.Error(w, err.Error(), 500)
}
fmt.Fprintln(w, "Succesfully updated user")
}
func deleteCustomer(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r)
id := v["id"]
fmt.Println("Deleting user: ", id)
if _, err := dbDriver.Exec("DELETE FROM customer where customer_id=$1", id); err != nil {
fmt.Println("Unable to delete the customer: ", err)
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintln(w, "Successfully deleted!")
}
func fetchCustomers(w http.ResponseWriter, r *http.Request) {
fmt.Println("Fetching all customers")
rows, err := dbDriver.Query("SELECT * from customer")
if err != nil {
fmt.Println("Unable to read the table: ", err)
http.Error(w, err.Error(), 500)
return
}
var customers []customer
defer rows.Close()
for rows.Next() {
var c customer
if err := rows.Scan(&c.CustomerID, &c.CustomerName); err != nil {
fmt.Println("Unable to scan")
}
customers = append(customers, c)
}
customerJSON, err := json.Marshal(customers)
if err != nil {
fmt.Println("Unable to marshall the data: ", err)
http.Error(w, err.Error(), 500)
return
}
fmt.Println("Customers: ", customers)
fmt.Fprintln(w, string(customerJSON))
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/customer", addCustomer).Methods("POST")
router.HandleFunc("/customer/{id}", updateCustomer).Methods("PUT")
router.HandleFunc("/customer/{id}", deleteCustomer).Methods("DELETE")
router.HandleFunc("/customer", fetchCustomers).Methods("GET")
log.Fatal(http.ListenAndServe(":8192", router))
}
You can checkout the entire code in this repository.
Now try the endpoints on Postman
or curl
.
Link to API documentation is here.
You can peek into the database once to see/cross-check how our data is stored which is the horse's mouth!
psql -U postgres -d guide
on the terminal. select * from customer
to see all the data that we've posted via our API. So, in just 130 lines of code we are created a fully functioning RESTful API performing all four of the CRUD operations in the most dominating language of this segment! kudos!
As further steps of deployment, you can write a simple Ansible script and a corresponding unit file which will keep your webserver running running and make systemd
take away all the pain of maintenance! As an example, you can have a look at this repository where I've implemented a server to store data in in-memory buffer
and deployed it to cloud using Ansible and an unit file.
Welcome, to the world of Go, hope this article got you started and now you can keep GOing.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!