Home/Blog/Programming/A step-by-step guide to building a RESTful API
Home/Blog/Programming/A step-by-step guide to building a RESTful API

A step-by-step guide to building a RESTful API

Ehtesham Zahoor
Jul 04, 2023
11 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

An application programming interface (API) is a software gateway that allows different software components to communicate with each other. APIs help expose the capabilities of an application to the outer world, allowing for programmatic access to their data.

Consider the case of an application providing stock or weather information. Building and exposing an API for such systems will allow others to programmatically fetch the data offered by these systems, such as weather forecasts for the provided location. The same example can be extended to various other use cases. Commonly used systems such as YouTube, Reddit, Google Maps, and others provide APIs, allowing authorized clients to access the resources these systems provide.

Over the years, APIs have evolved and have acquired some features that make them even more efficient and useful. Modern APIs conform to HTTP standards, which makes them developer friendly and easy to consume.

Representational state transfer (REST) is a web architecture style proposed in the year 2000 to address the web’s exponential scalability problem. REST requires a server to fulfill a client’s request by providing a representation of the resource, which contains links to change the system's state and acquire the representation of newer resources. The REST architecture style is derived from constraints, such as client-server communication, cacheability, statelessness, and others, and forms the basis of the modern internet.

APIs based on REST architecture are aptly called RESTful APIs. They commonly use HTTP as the underlying protocol, with HTTP methods (GET, POST, PUT, DELETE) for managing resources.

Building a RESTful API in Go#

At Educative, we have a diverse catalog of interactive and hands-on courses on APIs, and the topic of this blog is how to build a RESTful API.

We’ll use Go as the programming language. However, the concepts introduced in this blog can be applied to other programming languages as well.

The code is intentionally presented in a simplified way to help learners easily grasp the concepts. We have not handled errors or tried to optimize the code to ease readability and understanding.

We'll consider the example case of building an API to manage courses. We'll follow a step-by-step approach, and by the end of this blog, we'll have a fully functional API in Go. The video below represents how the API can be run on Educative's platffollow a step-by-step approach, and by the end of this blog, we'll have a fully functional API in Go. The video below represents how the API can be run on Educative's platform. orm.

Video thumbnail

As shown in the video above, we can launch the server and make GET requests using the browser. What about other HTTP requests, such as POST and DELETE? We can use one of the feature-rich widgets available on Educative's platform, the API widget. It can be used to make API requests with different HTTP methods and parameters. We'll present a demo at the end of this blog.

Step 1: API endpoints#

Before jumping right into the code, it would be helpful to familiarize ourselves with what we are trying to build and how to structure our API. We are building an in-memory course catalog and should be able to perform CRUD operations on courses. We need to map these CRUD operations to API endpoints when designing an API.

In bidirectional communication, an endpoint represents one end of the communication, essentially specifying how to reach the API using a URL. A client can send requests to an endpoint to retrieve resources or perform operations. The illustration above shows that endpoints have two parts: the base URL and the endpoint name.

Naming endpoints is important, and following best practices for naming endpoints will help others consume the API. In general, names should be (mostly plural) nouns based on the resource contents rather than a verb specifying the action performed on the resource. For our API, one possible naming convention is as follows:

HTTP request

Endpoint name

Description

GET

/courses

Returns the list of all the courses.

GET

/courses/:id

Retrieves information about a specific course, as specified by the ID parameter.

POST

/courses

Adds a new course to the collection. We can notice that the endpoint name stays the same.

PUT

/courses/:id

Updates the course with the specified ID. We can also use PATCH to partially update a course.

DELETE

/courses/:id

Deletes a specific course by providing its ID.

We have named our endpoint as /courses, a plural noun based on the contents of the resource available at the endpoint. We have also used a forward slash / to represent hierarchy, and /courses/:id represents a specific course instead of the collection. Further, we are using HTTP methods to specify the actions performed on the resource. Finally, we can also observe that some actions are performed on an individual resource instead of the complete collection.

Web application in Go #

Before moving on, let’s have a brief refresher on writing web applications in Go. Go provides a net/http package. The code below is for a "hello-world" web application in Go.

package main
import (
"fmt"
"net/http"
)
//the handler function
func getHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Educative!")
}
func main() {
http.HandleFunc("/", getHandler)
http.ListenAndServe(":8080", nil)
}
A simple web application in Go

Since we are building a standalone Go program, we need to place our code in package main. Inside the package main, let's start with the main function, the entry point to our code. The function first defines a handle function using http.HandleFunc, which specifies that all requests should be handled by a function named getHandler. The main function then uses http.ListenAndServe to start an HTTP server listening at the specified port, 8080.

This is clean, simple, and powerful. With these lines, our multi-threaded HTTP server is up and running, listening for HTTP requests and routing them to our handler. Next, let’s focus on our handle function, which is defined just before the main function. The function signature shows that it takes http.ResponseWriter and a http.Request as arguments. These are easier to understand. An http.Request variable represents the client's request while writing to http.ResponseWriter sends an HTTP response to the client.

Inside the function, we are simply writing a string to the client.

Step 2: GET all courses#

We are now ready to implement our first endpoint. We first need a data structure to hold courses' information. In our case, a struct (with ID and Title fields) would be sufficient at this stage. We can, of course, add other fields to the struct if needed.

//other fields can be added
type course struct {
ID string `json:"id"`
Title string `json:"title"`
}
//courses is a slice of course type
var courses = []course{
{"100","Grokking Modern System Design "},
{"101","CloudLab: WebSockets-based Chat Application using API Gateway"},
}

Tags such as json:"id" help us serialize the struct's content to JSON by using lower-case field names, which is a common style for JSON. We also instantiate a slice named courses containing some sample courses.

So we have the content ready to be consumed, and we need a way to deliver it to the clients requesting it over HTTP. We can update our handler from the "hello-world" code above to send the courses slice instead of a string.

//we are now sending courses instead of a string
func getHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<h1>%s</h1>", courses)
}
func main() {
http.HandleFunc("/courses/", getHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

That was super easy, but unfortunately, we are still not there. Although our code works as it should, the response is provided to the client in HTML, which is good for rendering on the browser but not for consumption. RESTful APIs typically provide the response in JSON, and we can easily marshal data using the encoding/json package. The updated function is as follows.

//Marshaling data and setting content-type header
func getHandler(w http.ResponseWriter, r *http.Request) {
jsonData, _ := json.Marshal(courses)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, "%s", jsonData)
}

With that, we are done with our first GET endpoint. If we run the server and access /courses, the endpoint on port 8080, from our browser, we'll see the JSON response. We can also notice that the output returned contains the field names in lowercase, which are the ones specified as tags with the struct definition.

Introducing a router#

Although we can continue building our API using the net/http package, it can get complicated to handle related cases. For instance, we need to use the same endpoint, /courses, with both GET and POST requests. As per our code above, the same handler will be called for both requests. One way around this can be that inside the handler, we use something like if r.Method == http.MethodGet { to handle different methods. To ease the development, we can use a third-party package such as github.com/gorilla/mux. Then, with gorilla, we can update the code as follows:

var mux *http.ServeMux = http.NewServeMux()
// register handlers to mux
mux.HandleFunc("/courses", getHandler).Methods("GET")
mux.HandleFunc("/courses", postHandler).Methods("POST")
Using github.com/gorilla/mux package

The gorilla/mux package has been widely used over the years. However, the github.com/gorilla/mux repository was archived by the owner in December 2022. We thus need a (better) alternative, and gin can help us. Gin is a Go-based web framework for building web applications; it also simplifies and improves many related aspects.

For the rest of this blog, we'll move away from the standard net/http package and use gin. We'll, however, keep comparing gin with net/http to highlight how it simplifies the development process.

Step 2a: GET all courses using gin#

Before moving forward, let's discuss how we can achieve the same functionality as in Step 2 using gin. The updated code, excluding the data structure and the courses slice for brevity's sake, is as below:

package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
/* removed struct declaration and courses instantiation for brevity */
func main() {
grouter := gin.Default()
grouter.GET("/courses", getcourses)
grouter.Run() //listens on 8080, by default
}
func getcourses(c *gin.Context) {
c.IndentedJSON(http.StatusOK, courses)
}
GET all courses using gin

We'll marvel at the capabilities of gin as we move along and build other routes. However, we can notice from the beginning how clean our code is compared to the earlier version. In the main function above, we initialize the router and specify a handler function. The handler function uses a pointer variable of type gin.Context representing request details and other associated information.

Calling c.IndentedJSON(http.StatusOK, courses) implicitly serializes our struct to JSON and writes an indented version back to the client along with the status code 200 OK. Alternatively, we can use c.JSON for a more compact JSON response. We are now all set to implement other endpoints as well.

Step 3: GET details of a specific course #

Our handler function, getcourses, currently returns the complete list of courses back to the client. What if we want to retrieve details of a specific course by providing its ID? Although we have some idea, based on what has gone above, of how this can be done, let's first revisit part of what we planned when defining endpoints.

HTTP request

Endpoint name

Description

GET

/courses/:id

The ID parameter can be used to retrieve information about a specific course.

The endpoint name /courses/:id includes the id parameter. The URL to access this endpoint would differ from the one used to access the /courses endpoint. As you have probably guessed, we first need to update our routing in the main function and introduce a new route for this case.

func main() {
grouter := gin.Default()
grouter.GET("/courses", getcourses)
//we have added a new route below
grouter.GET("/courses/:id", getSpecificCourse)
grouter.Run() //listens on 8080, by default
}

From the above code, we can see that we have added a new route definition. Once a user accesses the endpoint for a specific course, they are routed to the getSpecificCourse function. Here, we loop through the courses struct using the range keyword and send a response if the course ID matches the one sent in the request, as shown below:

func getSpecificCourse(c *gin.Context) {
//ID passed as a path parameter can then be retrieved
requestedId := c.Param("id")
for _, course := range courses {
if course.ID == requestedId {
c.IndentedJSON(http.StatusOK, course)
return
}
}
}

Notice that we are using a special syntax for specifying this route, /courses/:id, and any id passed as a path parameter can then be retrieved using c.Param(“id”).

Step 4: Handling POST to add a new course #

To add a new course to our collection, we'll use the same /courses endpoint. This time, we'll use the POST method with this endpoint. Let's take a moment to figure out how this can be done. It's not complicated, and you must have already figured out the process to follow. The first step is to add a new route definition to the main function:

func main() {
grouter := gin.Default()
grouter.GET("/courses", getcourses)
grouter.GET("/courses/:id", getSpecificCourse)
//we have added a new route below
grouter.POST("/courses", addCourse)
grouter.Run()
}

In the code above, we update our main function to handle POST requests. We can notice the addition of the POST method with the route handler. Next, we need to handle the request once it reaches the handler function, addCourse.

The handler function is, again, relatively simple. The core part is the invocation of the BindJSON function, which deserializes the course information sent with the request, in JSON, to the struct variable. Of course, the process can fail for malformed requests; we have already handled that case as well. The complete code for the addCourse function is shown below:

func addCourse(c *gin.Context) {
var courseToAdd course
err := c.BindJSON(&courseToAdd)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "malformed request!"})
}
courses = append(courses, courseToAdd)
c.IndentedJSON(http.StatusOK, courses)
}

Before we move on, take a moment to think about how this can be done using the standard net/http package and how gin simplifies and eases the process.

Step 5: PUT an updated version#

We can use the HTTP PUT method to update the resource (and PATCH for partially updating it). How can this be done? See the code below to figure out what needs to be done. If you are still unsure, the brief description for the next task will help you understand it fully.

func updateCourse(c *gin.Context) {
courseIdtoUpdate := c.Param("id")
var updatedCourse course
c.BindJSON(&updatedCourse)
for index, course := range courses {
if course.ID == courseIdtoUpdate {
courses[index] = updatedCourse
c.IndentedJSON(http.StatusOK, courses)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "course not found!"})
}
PUT an updated version

Step 6: DELETE a specific course#

Let's conclude this blog by discussing how to delete a course. The use of gin makes this straightforward. We specify a new handler with DELETE as the HTTP method and use id with a dynamic route. We then parse it inside the handler and remove it from the slice.

Here is the complete code:

package main
//we are using net/http and gin
import (
"net/http"
"github.com/gin-gonic/gin"
)
//data structure to hold course information
type course struct {
ID string `json:"id"`
Title string `json:"title"`
}
//initializing some courses
var courses = []course{
{"100", "Grokking Modern System Design "},
{"101", "CloudLab: WebSockets-based Chat Application using API Gateway"},
}
//main function contains routes for performing different operations
func main() {
grouter := gin.Default()
grouter.GET("/courses", getcourses)
grouter.GET("/courses/:id", getSpecificCourse)
grouter.POST("/courses", addCourse)
grouter.PUT("/courses/:id", updateCourse)
grouter.DELETE("/courses/:id", deleteCourse)
grouter.Run()
}
//GET all courses using gin
func getcourses(c *gin.Context) {
c.IndentedJSON(http.StatusOK, courses)
}
//GET details of a specific course
func getSpecificCourse(c *gin.Context) {
requestedId := c.Param("id")
for _, course := range courses {
if course.ID == requestedId {
c.IndentedJSON(http.StatusOK, course)
return
}
}
}
//Handling POST to add a new course
func addCourse(c *gin.Context) {
var courseToAdd course
err := c.BindJSON(&courseToAdd)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "malformed request!"})
}
courses = append(courses, courseToAdd)
c.IndentedJSON(http.StatusOK, courses)
}
func inefficientRemoval(srcSlice []course, index int) []course {
return append(srcSlice[:index], srcSlice[index+1:]...)
}
//DELETE a specific course
func deleteCourse(c *gin.Context) {
requestedId := c.Param("id")
courseID := -1
for index, course := range courses {
if course.ID == requestedId {
courseID = index
break
}
}
if courseID != -1 {
courses = inefficientRemoval(courses, courseID)
c.IndentedJSON(http.StatusOK, courses)
return
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "course not found!"})
}
//PUT an updated version
func updateCourse(c *gin.Context) {
courseIdtoUpdate := c.Param("id")
var updatedCourse course
c.BindJSON(&updatedCourse)
for index, course := range courses {
if course.ID == courseIdtoUpdate {
courses[index] = updatedCourse
c.IndentedJSON(http.StatusOK, courses)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "course not found!"})
}

Conclusion#

In this blog, we have provided a step-by-step guide on building RESTful APIs. We've considered the example case of building an API to manage courses and used Go as the programming language. However, the concepts introduced in this blog can be applied to other programming languages and mapped to other use cases as well.

This blog focused on RESTful APIs; however, other API architecture styles, including GraphQL and gRPC, have their strengths and use cases. We encourage you to explore them further.

Finally, we leave you with a demo of using Educative's API widget to invoke different endpoints of our example API.

Video thumbnail

  

Free Resources