What is REST
REST, acronym for Representational State Transfer, is the architectural approach to building web services. Let’s discuss a few characteristics of REST:
- REST is independent of any underlying protocol. i.e REST API can be built over any protocol like REST HTTP API which means RESTful API built over HTTP protocol.
- REST is independent of any specific implementation detail. i.e a REST API can be built on any language, tool, environment, etc. Almost every language allows one to write a REST API. The participating Client and Server can be built, modified and scaled separately, as long as they communicate in a consistent manner.
Design principles of RESTful HTTP APIs
- Statelessness: The participating client and server are not required to know the state of each other. Essentially, every request/response is independent and does not have any knowledge of the previous request/response.
- REST API endpoints are designed around resources which can be an object,service,data,etc. For example, an endpoint
/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) - A resource has an identifier in the URI which helps to uniquely identify the resource. Like
/customer/24
signifies the customer with a unique identifier24
. - The clients communicate with the service by exchanging representations of the resource. There are many exchange formats like XML,JSON,etc but these days JSON is heavily used (Javascript Object Notation).
- There are four basic HTTP verbs to communicate with the resources.
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.
Let’s start building
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 themain
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) Runpsql -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 columnscustomer_id
andcustomer_name
by runningcreate table customer(customer_id int,customer_name text);
after connecting theguide
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 runningmain
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, like8193
,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 makingINSERT
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 toDELETE
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.
Conclusion
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.