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.
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.
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.
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 |
|
| Returns the list of all the courses. |
|
| Retrieves information about a specific course, as specified by the ID parameter. |
|
| Adds a new course to the collection. We can notice that the endpoint name stays the same. |
|
| Updates the course with the specified ID. We can also use PATCH to partially update a course. |
|
| 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.
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 mainimport ("fmt""net/http")//the handler functionfunc getHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello from Educative!")}func main() {http.HandleFunc("/", getHandler)http.ListenAndServe(":8080", nil)}
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.
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 addedtype course struct {ID string `json:"id"`Title string `json:"title"`}//courses is a slice of course typevar 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 stringfunc 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 headerfunc 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.
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 muxmux.HandleFunc("/courses", getHandler).Methods("GET")mux.HandleFunc("/courses", postHandler).Methods("POST")
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.
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 mainimport ("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)}
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.
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 |
|
| 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 belowgrouter.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 retrievedrequestedId := 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”)
.
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 belowgrouter.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 courseerr := 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.
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 coursec.BindJSON(&updatedCourse)for index, course := range courses {if course.ID == courseIdtoUpdate {courses[index] = updatedCoursec.IndentedJSON(http.StatusOK, courses)return}}c.IndentedJSON(http.StatusNotFound, gin.H{"message": "course not found!"})}
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 ginimport ("net/http""github.com/gin-gonic/gin")//data structure to hold course informationtype course struct {ID string `json:"id"`Title string `json:"title"`}//initializing some coursesvar courses = []course{{"100", "Grokking Modern System Design "},{"101", "CloudLab: WebSockets-based Chat Application using API Gateway"},}//main function contains routes for performing different operationsfunc 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 ginfunc getcourses(c *gin.Context) {c.IndentedJSON(http.StatusOK, courses)}//GET details of a specific coursefunc 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 coursefunc addCourse(c *gin.Context) {var courseToAdd courseerr := 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 coursefunc deleteCourse(c *gin.Context) {requestedId := c.Param("id")courseID := -1for index, course := range courses {if course.ID == requestedId {courseID = indexbreak}}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 versionfunc updateCourse(c *gin.Context) {courseIdtoUpdate := c.Param("id")var updatedCourse coursec.BindJSON(&updatedCourse)for index, course := range courses {if course.ID == courseIdtoUpdate {courses[index] = updatedCoursec.IndentedJSON(http.StatusOK, courses)return}}c.IndentedJSON(http.StatusNotFound, gin.H{"message": "course not found!"})}
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.
Free Resources