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.
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.