👋🏻Hey, If the title of this article excites you, then my friend, you're at right place.
Let's quickly go through what we're going to cover in this post.
- First will be going to build a teeny weeny REST server and understand its working. After that, we'll incrementally improve its working and know its weakness.
- We build a CRUD server where we can
CREATE
,READ
,UPDATE
andDELETE
User details.
To keep everything simple. We'll focus on the basic concepts and won’t interact with any package and build server using the standard libraries and understand their limit and strength.
Prerequisites
You'll need Go version 1.16+ installed on your development machine.
Start 👷🏻♂️
Now let's dive into the code.
Version v1.0
package main
import (
"fmt"
"log"
"net/http"
)
func homePage(w http.ResponseWriter, r *http.Request) {
log.Println("Endpoint Hit: homePage")
fmt.Fprintf(w, "Welcome to the HomePage!")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homePage)
log.Fatal(http.ListenAndServe(":8090", mux))
}
If you run this code on your machine. This will start simple API server at port 8090
. In your browser navigate to http://localhost:8090/
, you'll going to see "Welcome to the HomePage!".
THE API IS CREATED 🎇
Ta-Da, You build your first API server with Go. Yes, It was that easy.
Ok, Now understand the code
func http.NewServeMux() *http.ServeMux
http.NewServeMux
is the default serve mux in Go from the net/http package. The method will actually create a multiplexer where it registers patterns with their corresponding function. We can also skip this step, as by default net/http package createDefaultServeMux
which is the same.
func (*http.ServeMux).HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
The
HandleFunc
method takes two arguments, the first string pattern, and the second handler function to which pattern to register with. Ok, but what the heck is a handler. Handler is an interface that has a method called ServeHttp, responds to an HTTP request, and has all the logic related to that End-point. We'll understand more about it in down the post.
func ListenAndServe(addr string, handler Handler) error
Then
ListenAndServe
method takes two arguments, the first is to address to start the server on and handler in the case of using DefaultServeMux we can pass nill as an argument.
Version v2.0
Now we're going to build REST server with all the functionality which will allow us to CREATE
, READ
, UPDATE
and DELETE
users.
User Structure
Before starting with anything first build the user structure and list in which our user will going to store in.
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
// let's declare a global Articles array
// that we can then populate in our main function
// to simulate a database
var userlist []User
You can see that we have use tags on struct field declarations to customize the encoded JSON key names. These tags are going to be used by "encoding/json"
package.
Create Helper function
JSON function is going to encode any data to JSON format so that we can send the data to the client.
func JSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.WriteHeader(statusCode)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
fmt.Fprintf(w, "%s", err.Error())
}
}
getUid will extract user-id(uid) from URL by slicing the URL. After that, it converts the uid to int from the string.
func getUid(pathSuffix string, r *http.Request) int {
var slug string
if strings.HasPrefix(r.URL.Path, pathSuffix) {
slug = r.URL.Path[len(pathSuffix):]
}
uid, _ := strconv.Atoi(slug)
return uid
}
Error is just a wrapper around JSON function to add an additional error flag to the JSON data
func Error(w http.ResponseWriter, statusCode int, err error) {
if err != nil {
JSON(w, statusCode, struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
return
}
JSON(w, http.StatusBadRequest, nil)
}
Building our Router
Now we'll add our routes to main.go
file.
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homePage)
mux.HandleFunc("/user/get/", getUser)
mux.HandleFunc("/user/post/", postUser)
mux.HandleFunc("/user/delete/", deleteUser)
mux.HandleFunc("/user/patch/", patchUser)
log.Fatal(http.ListenAndServe(":8090", mux))
}
Create
We will need to create a new function that will do the job of creating new users.
Let’s start off by creating a postUser()
function within our main.go
file.
func postUser(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: postUser")
reqBody, _ := ioutil.ReadAll(r.Body)
var userData User
json.Unmarshal(reqBody, &userData)
userlist = append(userlist, userData)
JSON(w, http.StatusCreated, "Created")
}
Now understand what we have written in the function.
- First of all, we've to extract the bytes array from the request body, for that we use
ioutil.ReadAll(r.Body)
which reads from r until an error or EOF and returns the data it read. json.Unmarshal(reqBody, &userData)
parses the JSON-encoded data and stores the result in the .userData.- After that we append the user into userlist.
- Send Created respone to the client
Read
Now we have created the creation function so now we want to fetch the newly created user from the server.
For that we'll create a getUser()
function within our main.go
file.
func getUser(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: getUser")
uid := getUid("/user/get/", r)
for _, user := range userlist {
if user.ID == uid {
JSON(w, http.StatusOK, user)
return
}
}
Error(w, http.StatusNotFound, errors.New("User Not Found"))
}
Now understand what we have written in the function.
- As client will send the user id in URL so we parse the URL and extract uid.
- After that we'll loop over the userlist array to find the user
- If we find one then we send OK status with user data.
- If the user is not present then send an Error response that the user is not found.
Update
After creating and reading now it's time for updating. create a patchUser()
function within our main.go
file.
func patchUser(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: patchUser")
uid := getUid("/user/patch/", r)
reqBody, _ := ioutil.ReadAll(r.Body)
var updatedUserData User
json.Unmarshal(reqBody, &updatedUserData)
for index, user := range userlist {
if user.ID == uid {
userlist[index] = updatedUserData
JSON(w, http.StatusOK, "Patched")
return
}
}
Error(w, http.StatusNotFound, errors.New("User Not Found"))
}
Delete
There may be times where you need to delete the data being exposed by your REST API. In order to do this, you need to expose a DELETE endpoint within your API that will take in an identifier and delete whatever is associated with that identifier.
Add a new function to your main.go file which we will call deleteUser()
:
func deleteUser(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: deleteUser")
uid := getUid("/user/delete/", r)
for index, user := range userlist {
if user.ID == uid {
userlist = append(userlist[:index], userlist[index+1:]...)
JSON(w, http.StatusOK, "Deleted")
return
}
}
Error(w, http.StatusNotFound, errors.New("User Not Found"))
}
Note - To keep this simple, we are updating a global variable. However, we aren’t doing any checks to ensure that our code is free of race conditions. In order to make this code thread-safe, I recommend checking out tutorial on Go Mutexes
if you're reading this, that means now you can build any CRUD application.
Conclusion
This example represents a very simple RESTful API written using Go. In a real project, we’d typically tie this up with a database so that we were returning real values. For the next step, I would suggest you should read Gorilla/Mux package. As it will remove a lot of boilerplate code and written only with the standard line.
Source Code - The full source code for this tutorial can be found here: KushagraMehta/Blog/REST server with Go
In the next post, we'll add Postgresql plus Dockerize the whole application, furthermore host it on Heroku/AWS.
I hope you find this blog useful. Please share your thought in the comments.