Building a REST-API in Go - Part I

Building a REST-API in Go - Part I

A REST API (Representational State Transfer Application Programming Interface) in the context of software development is a set of rules and conventions for building and interacting with web services. It is based on the principles of REST, which is an architectural style for designing networked applications. A REST API typically allows clients to perform CRUD (Create, Read, Update, Delete) operations on resources (which are typically represented as objects or collections of objects) using HTTP methods such as GET, POST, PUT, DELETE, etc. These resources are identified by unique URIs (Uniform Resource Identifiers).

Before building the REST API in Go, you need to know certain important Terminologies that are generally used in Software Development.

  1. Resources: These are the objects or data entities that the API deals with. For example, in a social media application, resources could include users, posts, comments, etc.

  2. HTTP Methods: REST APIs use HTTP methods to perform operations on resources. The most common methods are:

    • GET: Retrieve a resource or a collection of resources.

    • POST: Create a new resource.

    • PUT: Update an existing resource or create a new one if it doesn't exist.

    • DELETE: Remove a resource.

There are various other methods as well such as PATCH but for this blog, we'll stick to only four HTTP methods listed above.

  1. URLs (URIs): Each resource is typically represented by a unique URL. For example:

  2. HTTP Status Codes: These codes are returned with responses to indicate the status of the request. For example:

    • 200 OK: The request was successful.

    • 404 Not Found: The requested resource was not found.

    • 500 Internal Server Error: Indicates a server-side error.

There are various other status codes as well which can be found here.

  1. Representation: Resources are typically represented in JSON or XML format in the payload of the HTTP request/response.

Now, it's time to build our REST API in Go. Before you start, here are some more important things that you need to keep in mind:

  • This is Part 1 of building a REST API in Go, In future parts we'll take a look at how are we going to build the REST API using different efficient ways.

  • If your hands are not on the keyboard while reading this, you're probably doing something wrong. This means writing code alongside and trying to come up with different approaches.

  • You'll be going through certain methods in the code that might be new to you. Don't try to go in-depth about each method, instead try to understand 'what' is this trying to do and 'why' are we using it in the first place.

  • I'll keep all the pieces of code in one file main.go which is generally a bad practice. Since it's the first part of the series, we can try to use modules in the next part.

In this REST-API, I'll be trying to build a small system that is used to get information on the movies. I'll try to write an API that is used to perform certain operations on a URL, with movies used as resources. If this doesn't make sense to you right now, it's okay, just follow along.

Now, think what are the properties a Movie object can have. Done? It can have certain properties such as ID, Name of the movie, Director of the movie, Genre of the movie, and so on. This looks like a perfect use case for using a struct here. But a Director can have a first name as well as a last name too. Hence, our structs will look like this

type Movie struct {
    ID       int       `json:"id, string"`
    Name     string    `json:"name"`
    Genre    string    `json:"genre"`
    Director *Director `json:"director"`
}

type Director struct {
    FirstName string `json:"firstName"`
    LastName string `json:"lastName"`
}

Before moving ahead, let's try to look at the URLs that will be used to perform certain operations on the Movie object.

/ --> Display Home Page
/movies --> List all movies (GET)
/movies --> Add a new movie (POST)
/movies/{id} --> List a particular movie (GET)
/movies/{id} --> Update a particular movie (PUT)
/movies/{id} --> Delete a particular movie (DELETE)

In simple terms, the above endpoints are where the client request will hit. For eg. we'll have a URL such that whenever we write on our browser http://localhost:<port-num>/movies , we'll get a response in which all the movies we've defined will be displayed. Similarly, we can add a new movie on the same endpoint. Now, the question is how are we going to add a new movie by just typing the URL on the browser. That's where a tool like Postman comes in. Don't worry, we'll take a look at this later on.

Think from the First Principle thinking again, do we need to create a single movie object or a list of movies? Definitely a list of movies since there can be multiple movies that we can have. Um, would creating an array/slice be a good option?

Yes! We can create a slice of type Movie such that each object of the slice will contain the properties of the Movie.

var movies []Movie

Now movies will contain all the elements that we'll define.. Let's go ahead and define some movies in our main function. Try to do it yourself

var movies []Movie 

func main() {
    // movie1
    movie1 := Movie{
        ID:    1,
        Name:  "Dabaang",
        Genre: "Adventure",
        Director: &Director{
            FirstName: "Arbaaz",
            LastName:  "Khan",
        },
    }

    // movie2
    movie2 := Movie{
        ID:    2,
        Name:  "MS Dhoni: The untold story",
        Genre: "Sports",
        Director: &Director{
            FirstName: "Neeraj",
            LastName:  "Pandey",
        },
    }

    // movie3
    movie3 := Movie{
        ID:    3,
        Name:  "Deewar",
        Genre: "Adventure",
        Director: &Director{
            FirstName: "Yash",
            LastName:  "Chopra",
        },
    }
}

Now that we've defined three objects of type movie, we need to somehow store these movies in our Movies slice. Think about how can you do this.

movies = append(movies, movie1, movie2, movie2)

The append method tries to append the movie in a movies slice that accepts elements only of type Movie. What we've done till now is create a movie struct that will contain the properties a particular movie can have, such as a movie ID, name of the movie, genre of the movie, director of the movie and so on. Now, we need to work with these movies hence we created a new slice that will contain elements of type Movie. In our main function, we created movie objects (movie1, movie2, movie3) and finally append these objects in our movies slice

.

If you try to print the movies slice, you'll get the following response:

[{1 Dabaang Adventure 0xc000068020} {2 MS Dhone: The untold story Sports 0xc000068040} {3 Deewar Adventure 0xc000068060}]

Note that the response does not print the Director's first name and last name since it is a pointer to the Director struct and prints the address where the first name and last name of the director lie. In case you'd want to print the Director's name, don't reference the Director to Directory struct something like

type Movie struct {
    ID       int       `json:"id"`
    Name     string    `json:"name"`
    Genre    string    `json:"genre"`
    Director Director  `json:"director"`
}

Now that we have created our movies slice and appended elements as well, it's time to create handler functions that will be called whenever we try to hit a certain endpoint.

Let's start with creating a very basic handler function for the home page. What we want is whenever user hits the endpoint / , a greeting message should be displayed. Note that, we're going to have various other endpoints such as /movies , /movies/{id} . Hence, there should be some sort of way to transfer these requests to certain handler functions that will handle operations depending upon what endpoint we're trying to hit as a client and what method are we using. So now, we're going to use a router that will be used to transfer these requests to certain handlers. We're going to use the gorilla mux router.

In order to import this package in your piece of code, use the following command

go get -u github.com/gorilla/mux

Go through the documentation of the gorilla mux router and try to look how are you going to write a simple operation for your home page. You can do it using the following way

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomePage) // call the HomePage function at /
    http.ListenAndServe(":8080", router) // Listen on the port 8080
}

func HomePage(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Welcome to the home page"))
}

The above piece of code sets up a simple web server using Gorilla Mux, a library for handling HTTP requests. It creates a router to manage different URL paths. When someone visits the root URL "/", it calls the HomePage function, which sends a "Welcome to the home page" message to the visitor's web browser. The server listens on port 8080 for incoming connections

Run your piece of code using the command go run main.go and head over to your browser at http://localhost:8080/ , you'll see a greeting message displayed.

Nice! But now it's time to implement our main functions such as Get all the movies, Delete a movie, update a movie and so on. Let's try implementing the GetMovies function. Before writing the GetMovies function, you need to understand a little about Headers. Headers are additional information sent along with an HTTP request or response. They contain metadata about the message being sent, such as the content type, encoding, authentication tokens, and more.

For the GetMovies function, I'll first show you the code and try to explain by each line what this piece of code is doing and for the next set of functions, you'll try to implement these yourselves.

func GetMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    jsonBytes, err := json.Marshal(movies)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

GetMovies function is talking two arguments - w which is a responsewriter and r which is a pointer to the request.

The line w.Header().Add("Content-Type", "application/json") is setting the response header named "Content-Type" to indicate that the content being sent back in the HTTP response is in JSON format. In other words, it tells the client (like a web browser or another application) that the data it's receiving will be in JSON format. This is helpful for the client to understand how to interpret the data properly.

You need to convert the data that is sent by the server to your browser in a way that you/your browser understands. This is where JSON comes in. In the next line, you're converting the response sent by the server into JSON format using the json.Marshal function. You're trying to convert the movies slice into a JSON format and checking for an error. Finally, if there is no error, you're mentioning in the header that the status is OK, that is, Status 200, and finally you're writing(displaying) the JSON response sent to you by the server on the web browser.

Finally, your main function will look something like

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomePage) // call the HomePage function at /
    router.HandleFunc("/movies", GetMovies).Methods("GET")
    http.ListenAndServe(":8080", router) // Listen on the port 8080
}

func GetMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    jsonBytes, err := json.Marshal(movies)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

Let's try to write the function in order to get a single movie object i.e GetMovie. Imagine I pass the URL something like https://localhost:8080/movies/1 , the response that I should get is the information about the movie that has the ID as 1. We need to consider certain scenarios where our id does not match the params we pass in the URL. Try to look on google how are we going to fetch the id that we'll be passing in the URL. Done?

If I wanted to display the id from “/movies/{id}”, I would use mux.Vars(). This results in a map that will contain the key as "id" and value of the key is what we pass in the URL. But the value of the key id stored in the map will be in the string format, hence in order to compare this with the movie ID, it should be converted to integer value. This can be done using strconv package.

func GetMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    params := mux.Vars(r)
    res := params["id"]
    id, err := strconv.Atoi(res)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }
}

Good, now you've fetched the id from the URL. The only left is to check whether this id that you've fetched from the URL matches with any of the Movies ID. If it does, convert that movie object into JSON and send it back as a response. Simple, Isn't it? Hence, our GetMovie function will now look like

func GetMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    params := mux.Vars(r) // get the params you pass in request body
    res := params["id"]
    id, err := strconv.Atoi(res) // convert the string into integer
    if err != nil {
        log.Fatalf("%v", err)
        return
    }
    for _, movie := range movies { // ignore the index here
        if movie.ID == id {
            jsonBytes, err := json.Marshal(movie)
            if err != nil {
                log.Fatalf("%v", err)
                return
            }

            w.WriteHeader(http.StatusOK)
            w.Write(jsonBytes)
            break // if id matches, don't check for other elements
        }
    }
    fmt.Printf("SUCCESS!")
}

Our main function will look this way now

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomePage) // call the HomePage function at /
    router.HandleFunc("/movies", GetMovies).Methods("GET")
    router.HandleFunc("/movies/{id}", GetMovie).Methods("GET")
    http.ListenAndServe(":8080", router) // Listen on the port 8080
}

Go ahead and try to write the function for Deleting a particular movie now. Search for way on Google how are you going to delete an element in a slice in Go. Maybe this answer can be helpful for you.

You need to make sure that the id you pass in the URL should eventually delete the movie of that particular id with HTTP Method set to DELETE. The function will almost look similar to getting a particular movie and looks something like

func DeleteMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    params := mux.Vars(r)
    res := params["id"]
    id, err := strconv.Atoi(res)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }

    for index, movie := range movies { // we need the movie index to delete that index
        if movie.ID == id {
            movies = append(movies[:index], movies[index+1:]...)
            jsonBytes, err := json.Marshal(movie)
            if err != nil {
                log.Fatalf("%v", err)
                return
            }

            w.WriteHeader(http.StatusOK)
            w.Write(jsonBytes)
            break
        }
    }
}

Our main function will look like this now

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomePage) // call the HomePage function at /
    router.HandleFunc("/movies", GetMovies).Methods("GET")
    router.HandleFunc("/movies/{id}", GetMovie).Methods("GET")
    router.HandleFunc("/movies/{id}", DeleteMovie).Methods("DELETE")
    http.ListenAndServe(":8080", router) // Listen on the port 8080
}

Now, we're left with mostly two functions which are CreateMovie and UpdateMovie. Let's go ahead and write the code for CreateMovie. Before writing the function for creating a movie, think what information we will be required here. Ideally, we will be sending some piece of information in our request body such as id of the movie, name of the movie and so on in a JSON format. Once this information has been sent to the server, the server should decode this JSON information, and check if the movie ID already exists. If movie ID doesn't exist, just append the movie we've sent in the request body to the movies slice.

The function for CreateMovie will look something like

func CreateMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    var newMovie Movie // this is the movie we'll be sending in request body

    // check if a movie id has the same id as new movie
    for _, movie := range movies {
        if movie.ID == newMovie.ID {
            w.Write([]byte("Movie ID already exists"))
            return
        }
    }

    // Decode the new movie on server side from JSON and check for error
    if err := json.NewDecoder(r.Body).Decode(&newMovie); err != nil {
        log.Fatalf("%v", err)
        return
    }

    // Finally append that newMovie in our movies slice
    movies = append(movies, newMovie)

    // Encode (convert to JSON) the movies slice
    json.NewEncoder(w).Encode(movies)
    w.WriteHeader(http.StatusOK)
}

Remember a simple rule, whenever you're sending something in the request body, you need to first decode it from JSON(generally happens on the server side) and finally encode into the JSON format to send it as a response.

Our main function will look like this now

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomePage) // call the HomePage function at /
    router.HandleFunc("/movies", GetMovies).Methods("GET")
    router.HandleFunc("/movies/{id}", GetMovie).Methods("GET")
    router.HandleFunc("/movies/{id}", DeleteMovie).Methods("DELETE")
    router.HandleFunc("/movies", CreateMovie).Methods("POST")
    http.ListenAndServe(":8080", router) // Listen on the port 8080
}

The main question is how are we going to test these create and delete methods. Head over to the Postman, create a workspace, enter the URL, choose the method you'd like to use, and adjust if you'd like something to pass in the request body. Finally, send the request and look at the response on Postman itself.

The last method that we need to implement is UpdateMovie. What we want here is to update an existing movie. For example, maybe I want to update the name of the movie with ID equals to 2 to some other name. Think of how are you going to implement the UpdateMovies method. Let me try to give you a hint, of all the things you've learnt so far, you'll be using the same in the UpdateMovie method.

A basic approach would be something like:

  • Check if the movie id already exists.

    • If it doesn't, either call the CreateMovie method or maybe return the function. I'll just return the function but you can try to create the movie instead.
  • Now, that the movie id exists

    • Delete the existing movie

    • create a new movie with same id and updated information

    • add it to the movies slice

Try to implement this functionality by your own. The above piece of code will look like this

func UpdateMovie(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)
    res := params["id"]
    id, _ := strconv.Atoi(res)

    // check if movie exist already
    for index, movie := range movies {
        if movie.ID == id {
            // delete the existing movie
            movies = append(movies[:index], movies[index+1:]...)
            // create a new movie
            var newMovie Movie
            // Decode the new movie on the server side
            json.NewDecoder(r.Body).Decode(&newMovie)
            // append the new movie in the movies slice
            movies = append(movies, newMovie)
            // Encode the new movie and send it back as a response
            json.NewEncoder(w).Encode(newMovie)
            return
        }
    }

    fmt.Fprintf(w,"Movie of id %d does not exist",id)
}

Our main function will look like this now

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomePage) // call the HomePage function at /
    router.HandleFunc("/movies", GetMovies).Methods("GET")
    router.HandleFunc("/movies/{id}", GetMovie).Methods("GET")
    router.HandleFunc("/movies/{id}", DeleteMovie).Methods("DELETE")
    router.HandleFunc("/movies", CreateMovie).Methods("POST")
    router.HandleFunc("/movies/{id}", UpdateMovie).Methods("PUT")
    http.ListenAndServe(":8080", router) // Listen on the port 8080
}

You can test this functionality again on the Postman itself. Hence our final code will look something this way

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "github.com/gorilla/mux"
    "net/http"
    "strconv"
)

type Movie struct {
    ID       int       `json:"id,string"`
    Name     string    `json:"name"`
    Genre    string    `json:"genre"`
    Director *Director `json:"director"`
}

type Director struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
}

var movies []Movie

func main() {
    movie1 := Movie{
        ID:    1,
        Name:  "Dabaang",
        Genre: "Adventure",
        Director: &Director{
            FirstName: "Arbaaz",
            LastName:  "Khan",
        },
    }
    movie2 := Movie{
        ID:    2,
        Name:  "MS Dhone: The untold story",
        Genre: "Sports",
        Director: &Director{
            FirstName: "Neeraj",
            LastName:  "Pandey",
        },
    }
    movie3 := Movie{
        ID:    3,
        Name:  "Deewar",
        Genre: "Adventure",
        Director: &Director{
            FirstName: "Yash",
            LastName:  "Chopra",
        },
    }
    movies = append(movies, movie1, movie2, movie3)

    router := mux.NewRouter()

    router.HandleFunc("/", HomePage)
    router.HandleFunc("/movies", GetMovies).Methods("GET")
    router.HandleFunc("/movies/{id}", GetMovie).Methods("GET")
    router.HandleFunc("/movies/{id}", DeleteMovie).Methods("DELETE")
    router.HandleFunc("/movies", CreateMovie).Methods("POST")
    router.HandleFunc("/movies/{id}", UpdateMovie).Methods("PUT")
    http.ListenAndServe(":8080", router)
}

func HomePage(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Welcome to the home page"))
}

func GetMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    jsonBytes, err := json.Marshal(movies)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

func GetMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    params := mux.Vars(r)
    res := params["id"]
    id, err := strconv.Atoi(res)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }
    for _, movie := range movies {
        if movie.ID == id {
            jsonBytes, err := json.Marshal(movie)
            if err != nil {
                log.Fatalf("%v", err)
                return
            }

            w.WriteHeader(http.StatusOK)
            w.Write(jsonBytes)
            break
        }
    }
    fmt.Printf("SUCCESS!")
}

func DeleteMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    params := mux.Vars(r)
    res := params["id"]
    id, err := strconv.Atoi(res)
    if err != nil {
        log.Fatalf("%v", err)
        return
    }

    for index, movie := range movies {
        if movie.ID == id {
            movies = append(movies[:index], movies[index+1:]...)
            jsonBytes, err := json.Marshal(movie)
            if err != nil {
                log.Fatalf("%v", err)
                return
            }

            w.WriteHeader(http.StatusOK)
            w.Write(jsonBytes)
            break
        }
    }
}

func CreateMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    var newMovie Movie

    for _, movie := range movies {
        if movie.ID == newMovie.ID {
            w.Write([]byte("Movie ID already exists"))
            return
        }
    }

    if err := json.NewDecoder(r.Body).Decode(&newMovie); err != nil {
        log.Fatalf("%v", err)
        return
    }

    movies = append(movies, newMovie)
    json.NewEncoder(w).Encode(movies)
    w.WriteHeader(http.StatusOK)
}

func UpdateMovie(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)
    res := params["id"]
    id, _ := strconv.Atoi(res)

    // check if movie exist already
    for index, movie := range movies {
        if movie.ID == id {
            movies = append(movies[:index], movies[index+1:]...)
            var newMovie Movie
            json.NewDecoder(r.Body).Decode(&newMovie)
            movies = append(movies, newMovie)
            json.NewEncoder(w).Encode(newMovie)
            return
        }
    }

    fmt.Fprintf(w,"Movie of id %d does not exist",id)
}

You can also find all pieces of code in this GitHub Repository.

If you like reading this and building this, head over to another part of the series in which we'll look for other efficient ways to build the same REST API. If you'd like to get connect with me, reach out to me on Twitter or LinkedIn.