Writing RESTful APIs in Go Without Dependencies

3736 VIEWS

· · ·

This article will walk you through building faster RESTful APIs in Go without external dependencies. You’ll learn how to build an independent RESTful API using a simple API CRUD (create, read, update, delete) functionalities.

RESTful API is one of the most used architectures for communication between computers over the internet. The RESTful architecture is a client-server architecture that defines that APIs should be stateless, cacheable, and apply code on-demand principles. 

Many backend frameworks (Mux, Fiber, Fasthttp, Gin) help build RESTful APIs in Go. However, these backend frameworks wrap the net package to enable users to build RESTful APIs in Go while trading off a bit of speed, flexibility, and power.

The net package contains HTTP, SMTP, RPC, and other packages for network I/0- related operations. Using the net/http package for building RESTful APIs without dependencies isn’t as difficult or time-consuming as it may seem. 

Building Independent RESTful APIs in Go 

You’ll need to have Go installed on your machine to follow this tutorial. Create a Go workspace and initialize the Go modules file using the command below. 

go mod init "github.com/goodnessuc/no_dependencies

You don’t need to install any packages or dependencies using this tutorial. The net/http package is part of the standard library. All you have to do is import it to your Go workspace and use it.

import "net/http"

Like most RESTful APIs where the response is in JSON format, the API you’ll be building in this tutorial will respond with JSON data. For this, you’ll need a struct that you’ll parse to JSON in the handler functions.

type Person struct { 
name string 
age int 
}

The Person struct will be the blueprint for responses and requests to your API. 

You’ll be using a slice as the data store in this tutorial. You want to set the slice as a global variable to access it from the handler functions and operate on it. 

var store = make([]Person, 0)

The make function would create an empty slice with no values, and you can insert values using the append function.

The next step is to define API routes for communicating with the server.

Routing API EndPoints Using the net/http Package 

API endpoints are the entry points for communication between a client and a server. The client makes a call to the server that returns a response or executes the operation from the client if the call is API valid. 

You can route endpoints using the HandleFunc method of the net/http package. The HandleFunc method takes in the route and a handler function. 

http.HandleFunc("/api/v1/getName", handleGetPerson) 
http.HandleFunc("/api/v1/updateName", UpdatePerson) 
http.HandleFunc("/api/v1/addName", handleAddPerson) 
http.HandleFunc("/api/v1/deleteName", handleDeletePerson)

The routes are the arguments in the form api/v1/, and the second arguments are the names of the handler functions you’ll be learning how to implement later in this article.

The next step is to start a server that listens to requests to the endpoint. You can start a local server using the ListenAndServe method. The ListenAndServe method takes in an address and handler and returns an error if there’s any. 

err := http.ListenAndServe(":8080", nil) 
if err != nil { 
log.Fatalln("There's an error with the server," err) 
}

Setting Up the Handler Functions

You’ll build the CRUD functionalities of your API in the handler function. There will be four handler functions. A handler function that returns a Person, another function that adds a Person to the data store, a function that deletes a Person entry from the data store, and one that updates a Person entry in the data store.

The POST Request Handler Function 

POST requests are solely for receiving data through the API. A POST request would add the data to the datastore and return a response based on the request’s status. 

All your handler functions have to take in a writer and a request in this form. 

func handleAddPerson(writer http.ResponseWriter, request *http.Request) { 
writer.Header().Set("Content-Type", "application/json") 
}

You’ll use the writer parameter to write and format responses to the client; the request parameter is for accepting request-related data from the client. In the function’s body, you’ve set your response content type to be in JSON format. 

You have to verify that the request method matches what your handler function does. You can check the request method using the Method method of the request parameter. 

if request.Method == "POST" { 
// handler function code
}

In the code example, you verified that the request for the handler function is a POST request. You’ll have to verify the type of request in every handler function.

func handleAddPerson(writer http.ResponseWriter, request *http.Request) { 
writer.Header().Set("Content-Type", "application/json") 
if request.Method == "POST" { 
writer.WriteHeader(http.StatusOK) 
var human Person 
err := json.NewDecoder(request.Body).Decode(&human) 
if err != nil { 
log.Fatalln("There was an error decoding the request body into the struct") 
} 
store = append(store, human) 
err = json.NewEncoder(writer).Encode(&human) 
if err != nil { 
log.Fatalln("There was an error encoding the initialized struct") 
} 
} else { 
writer.WriteHeader(http.StatusBadRequest) 
writer.Write([]byte("Bad Request")) 
} 
}

In the handleAddPerson function, after verifying that the request method is POST, the writer writes StatusOk to the client to signify that the request was accepted. The request body was decoded into the human variable using the Decode method of the NewDecoder method of the json package, and possible errors were handled in case the request body was wrongly formatted. 

The decoded struct is saved in the store slice in this case. The function writes the request body back to the client as a response to acknowledge that the data was saved using the Encode method of the NewEncoder method that takes in the writer. 

If the request method isn’t POST, the function sends a bad request status, and the writer writes a “Bad Request” as a response to the client. 

On testing the addName endpoint, the status is 200 OK, and the response is the request body as the handleAddPerson function should. 

The GET Request Handler Function 

GET requests are for querying APIs for data. A GET request should return a response based on the query parameter if the request is valid. Here’s how you can build a GET request handler function using the net/http package. 

func handleGetPerson(writer http.ResponseWriter, request *http.Request) { 
writer.Header().Set("Content-Type", "application/json") 
if request.Method == "GET" { 
name := request.URL.Query()["name"][0] 
for _, structs := range store { 
if structs.Name == name { 
err := json.NewEncoder(writer).Encode(&structs) 
if err != nil {
log.Fatalln("There was an error encoding the initialized struct") 
} 
} 
} 
} else { 
writer.WriteHeader(http.StatusBadRequest) 
writer.Write([]byte("Bad Request")) 
} 
}

The handleGetPerson uses the URL method of the request parameter to query for the name parameter. Then, using a range for-loop, to search by traversing the store slice. If any structs in the store slice have the name, it is returned as the response using the JSON encoder. The function also returns the StatusBadRequest method if the request method isn’t GET

The PUT Request Handler Function 

PUT requests are for updating the datastore’s existing values. The body of the PUT request will contain similar values and the data to be changed. 

func UpdatePerson(writer http.ResponseWriter, request *http.Request) { 
writer.Header().Set("Content-Type", "application/json") 
if request.Method == "PUT" { 
var human Person 
err := json.NewDecoder(request.Body).Decode(&human) 
if err != nil { 
log.Fatalln("There was an error decoding the request body into the struct") 
} 
for index, structs := range store { 
if structs.Name == human.Name { 
store = append(store[:index], store[index+1:]...) 
} 
} 
store = append(store, human) 
err = json.NewEncoder(writer).Encode(&human) 
if err != nil { 
log.Fatalln("There was an error encoding the initialized struct") 
} 
} else { 
writer.WriteHeader(http.StatusBadRequest) 
writer.Write([]byte("Bad Request")) 
} 
}

In the UpdatePerson handler, after verifying that the request method is PUT, the request body was decoded into the human struct in the same way as in the POST request, and then you’ll use a for-loop to traverse the data store and update the values.

In this case, since we are using a slice of structs, the for loop deletes the initial value and then inserts the new one when it finds the match. If you’re using a database, there’ll be functionality for updating values without deleting them. 

The DELETE Request Handler 

DELETE requests are for deleting values from the data store. DELETE requests are similar to GET requests such that you’ll need a unique parameter to delete values from the data store. In this case, you’ll use the name field to delete entries from the data store. 

func handleDeletePerson(writer http.ResponseWriter, request *http.Request) { 
writer.Header().Set("Content-Type", "application/json") 
if request.Method == "DELETE" { 
name := request.URL.Query()["name"][0] 
indexChoice := 0 
for index, structs := range store { 
if structs.Name == name { 
indexChoice = index 
} 
} 
store = append(store[:indexChoice], store[indexChoice+1:]...) 
writer.Write([]byte("Deleted It")) 
} else { 
writer.WriteHeader(http.StatusBadRequest) 
writer.Write([]byte("Bad Request")) 
} 
}

The handleDeletePerson handler function verifies that the request is a DELETE request, and the name variable gets the name parameter from the request URL. The range for loop traverses the struct, searches for the data entry, and deletes the struct from the store slice. The function writes “Deleted It” to the client as a successful response, as shown below.

RESTful APIs in Go without dependencies - Successful Response

Conclusion

RESTful APIs are everywhere, and you’re familiar with many services using RESTful APIs for communication. 

This tutorial covered how to build a simple CRUD RESTful API in Go without dependencies, using the net/http package, which is part of the standard library. In real-world cases, you’ll be using a database to build a RESTful API. You can choose a SQL database and use an ORM or NoSQL database like MongoDB for smooth database interactions and storage.


Ukeje Goodness Chukwuemeriwo is a Software developer and DevOps Enthusiast writing Go code day-in day-out.


Discussion

Click on a tab to select how you'd like to leave your comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Menu
Skip to toolbar