How to build an API using Go
Ekemini Samuel
Posted on April 15, 2023
Have you ever felt building an API is like creating a secret code only a select few can understand? As a web developer👨🏾💻 and tech writer📝, I've been there. But fear not, my friend! In this article, I'll show you how Go can help you unlock the mysteries of API development and make it accessible to anyone.
So let's dive into the world of APIs and Go, and discover how you can create powerful and efficient APIs with ease.
Explanation of what an API is and why it’s useful
An API is a set of protocols, routines, and code used for building software applications. APIs specify how software components should interact and communicate with each other, enabling developers to build complex software systems more easily. APIs provide a standardized way for software to exchange data and perform functions, making it easier for developers to create new applications, integrate existing systems, and automate processes.
APIs are useful because they allow developers to create applications that can interact with other software systems, services, and data sources. This means that developers don't have to start from scratch every time they want to build a new application, and they can leverage existing code, data, and functionality to create new and innovative products. APIs also enable developers to build software products that are scalable, modular, and maintainable, making it easier to add new features and functionality over time.
A brief description of Go and its benefits for building APIs
Go is a modern programming language developed by Google that is gaining popularity among developers for building APIs. Go is a compiled language, which means that it produces fast and efficient code that can run on a variety of platforms. Go is also designed to be easy to read and write, making it a great choice for building large and complex software systems.
One of the key benefits of using Go for building APIs is its excellent support for concurrency. Concurrency is the ability to run multiple tasks at the same time, and Go makes it easy to write code that can take advantage of multi-core processors and other hardware resources. This means that Go APIs can handle high levels of traffic and requests without slowing down or crashing.
Another benefit of using Go for building APIs is its built-in support for JSON (JavaScript Object Notation), a lightweight data format that is widely used for data exchange in web applications. Go's support for JSON makes it easy to create APIs that can accept and return data in this format, simplifying the process of building and integrating APIs with other software systems.
Overall, Go is a powerful and flexible language that is well-suited for building APIs. Its speed, concurrency support, and built-in JSON support make it a great choice for developers looking to create scalable, high-performance APIs.
Setting up the Go environment
- Installing Go
The first step is to download and install Go on your machine. You can download the latest version of Go from the official Go website. Once downloaded, follow the installation instructions for your operating system.
- Configuring the environment variables
After installing Go, you need to configure the environment variables. In Windows, you can do this by right-clicking on "My Computer" and selecting "Properties". From there, click on "Advanced system settings" and then "Environment Variables". Add the path to the Go bin folder to the "Path" variable.
In Linux and macOS, you need to edit the .bashrc
or .bash_profile
file in your home directory and add the following lines:
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
- Setting up a workspace
Next, you need to set up a workspace for your Go projects. A workspace is a directory that contains your Go source code files and binaries.
Create a directory called go-workspace
in your home directory:
$ mkdir ~/go-workspace
Inside this directory, create three subdirectories: src
, pkg
, and bin
.
$ cd ~/go-workspace
$ mkdir src pkg bin
You're ready to start building APIs with Go!
Building the API
- Choosing a framework (e.g., Gorilla mux, Echo, Gin)
Now that we have set up the Go environment, we can start building our API. The first step is to choose a framework. There are several popular frameworks for building APIs in Go, such as Gorilla mux, Echo, and Gin. For this article, we'll use Gorilla mux
.
- Creating a basic server
To create a basic server, we need to import the necessary packages and define a main function. The main function is the entry point for our program. Here's an example of a basic server using Gorilla mux:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
})
http.ListenAndServe(":8000", router)
}
This code imports the "fmt", "net/http", and "github.com/gorilla/mux" packages, defines a main function that creates a new router using Gorilla mux, adds a handler function for the "/" route that writes "Hello, World!" to the response writer, and starts an HTTP server that listens on port 8000.
- Adding routes and handlers for different endpoints
To add routes and handlers for different endpoints, we need to define additional handler functions and register them with the router.
// Define a handler function for the /users endpoint
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
// Get the list of users from the database
users := getUsersFromDB()
// Convert the list of users to JSON format
usersJSON, err := json.Marshal(users)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set the content type of the response to JSON
w.Header().Set("Content-Type", "application/json")
// Write the JSON response to the client
w.Write(usersJSON)
}
// Register the /users endpoint with the router
r.HandleFunc("/users", getUsersHandler).Methods(http.MethodGet)
In this example, we define a getUsersHandler
function that retrieves a list of users from a database, converts it to JSON format, and writes it to the response. We then register this handler function with the router by calling r.HandleFunc("/users", getUsersHandler).Methods(http.MethodGet)
.
We can similarly define and register handler functions for other endpoints, such as /users/{id}
for retrieving a specific user by ID, /users
with HTTP POST method for creating a new user, /users/{id}
with HTTP PUT method for updating a user, and /users/{id}
with HTTP DELETE method for deleting a user.
- Implementing CRUD operations:
To implement CRUD operations (Create, Read, Update, Delete), we need to define handler functions for each operation and register them with the router.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
type Book struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Author string `json:"author,omitempty"`
Publisher *Company `json:"publisher,omitempty"`
}
type Company struct {
Name string `json:"name,omitempty"`
Address string `json:"address,omitempty"`
}
var books []Book
func GetBooks(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(books)
}
func GetBook(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
for _, item := range books {
if item.ID == params["id"] {
json.NewEncoder(w).Encode(item)
return
}
}
json.NewEncoder(w).Encode(&Book{})
}
func CreateBook(w http.ResponseWriter, r *http.Request) {
var book Book
_ = json.NewDecoder(r.Body).Decode(&book)
books = append(books, book)
json.NewEncoder(w).Encode(book)
}
func UpdateBook(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
for index, item := range books {
if item.ID == params["id"] {
books = append(books[:index], books[index+1:]...)
var book Book
_ = json.NewDecoder(r.Body).Decode(&book)
book.ID = params["id"]
books = append(books, book)
json.NewEncoder(w).Encode(book)
return
}
}
json.NewEncoder(w).Encode(books)
}
func DeleteBook(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
for index, item := range books {
if item.ID == params["id"] {
books = append(books[:index], books[index+1:]...)
break
}
}
json.NewEncoder(w).Encode(books)
}
func main() {
router := mux.NewRouter()
books = append(books, Book{ID: "1", Title: "Book One", Author: "John Doe", Publisher: &Company{Name: "Publisher One", Address: "Address One"}})
books = append(books, Book{ID: "2", Title: "Book Two", Author: "Jane Smith", Publisher: &Company{Name: "Publisher Two", Address: "Address Two"}})
router.HandleFunc("/books", GetBooks).Methods("GET")
router.HandleFunc("/books/{id}", GetBook).Methods("GET")
router.HandleFunc("/books", CreateBook).Methods("POST")
router.HandleFunc("/books/{id}", UpdateBook).Methods("PUT")
router.HandleFunc("/books/{id}", DeleteBook).Methods("DELETE")
log.Fatal(http.ListenAndServe(":8000", router))
}
This code defines a Book
struct with ID
, Title
, Author
, and Publisher
fields. It then defines handler functions for getting all books, getting a specific book by ID, creating a new book, updating an existing book, and deleting a book. These handler functions use Gorilla mux's Vars
function to extract variables from the URL, and they use the json
package to encode and decode JSON data. Finally, a router is set up and registers the handler functions with the appropriate HTTP methods and URL paths, allowing the API to receive and respond to requests. This creates a fully functional RESTful API that can be deployed to a server and used by clients to perform CRUD operations on a collection of books.
With this example, you can start building your own APIs using Go and Gorilla mux, and customize them to fit your specific needs.
Handling errors and exceptions
Handling errors and exceptions is an essential aspect of building any software, and APIs are no exception. When building an API, it's important to anticipate and handle errors and exceptions that may occur during runtime.
Some techniques for handling errors and exceptions in a Go API are:
- Error handling in handler functions:
One way to handle errors and exceptions in a Go API is by using error handling in handler functions.
func getUser(w http.ResponseWriter, r *http.Request) {
// ...code to get user by ID...
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ...return user as JSON...
}
If an error occurs while getting the user, it will be returned as an HTTP 500 Internal Server Error.
- Custom error types:
In Go, you can define custom error types to represent specific errors that can occur in your API.
type UserNotFoundError struct {
ID int
}
func (e UserNotFoundError) Error() string {
return fmt.Sprintf("user with ID %d not found", e.ID)
}
A custom error type UserNotFoundError
is defined to represent the error when a user with a given ID is not found. It can be used in the handler functions:
func getUser(w http.ResponseWriter, r *http.Request) {
// ...code to get user by ID...
if err != nil {
if _, ok := err.(UserNotFoundError); ok {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ...return user as JSON...
}
If an error of type UserNotFoundError
occurs, it will be returned as an HTTP 404 Not Found.
- Panic and recover:
Another way to handle errors and exceptions in a Go API is by using the panic and recover mechanism.
func getUser(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Println("recovered from panic:", r)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// ...code that may panic...
}
If a panic occurs in the handler function, it will be caught and logged, and an HTTP 500 Internal Server Error will be returned to the client.
By implementing these techniques, you can ensure that your Go API is robust and can handle errors and exceptions gracefully.
Integrating with a database
Integrating with a database is a crucial step in building a production-ready API. There are many databases to choose from, such as MySQL, PostgreSQL, MongoDB, and more. In this section, we'll go over the steps for integrating Go with a database.
- Choosing a database (e.g., MySQL, PostgreSQL, MongoDB)
The choice of database depends on the specific needs of the project. For example, if the application requires a highly relational data model, then a SQL-based database such as MySQL or PostgreSQL might be the best choice. On the other hand, if the data is more document-oriented or unstructured, a NoSQL database such as MongoDB might be a better fit. It's essential to choose a database that fits the project's needs while considering factors such as performance, scalability, and cost.
- Installing necessary drivers
To integrate with a specific database, we need to install the appropriate driver or package. For example, to use MySQL, we can install the "go-sql-driver/mysql" package. Similarly, we can use the "pq" package to connect to PostgreSQL, or the "mongo-go-driver" package to connect to MongoDB.
- Creating a database connection
Once the necessary package is installed, we can create a database connection. The connection string usually contains the credentials needed to connect to the database, such as the username, password, and database name.
Creating a connection to a MySQL database:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(host:port)/database")
if err != nil {
// handle error
}
defer db.Close()
// do something with db
}
- Implementing database operations (e.g., querying, inserting, updating, deleting)
Database operations such as querying, inserting, updating, and deleting data, can be performed after a connection has been made.
A function that retrieves all users from a MySQL database:
func getUsers(db *sql.DB) ([]User, error) {
var users []User
rows, err := db.Query("SELECT * FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var user User
err := rows.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
Integrating Go with a database is a crucial step in building a production-ready API.
Choose the appropriate database, install the necessary drivers, create a connection, and implement database operations.
Adding authentication and authorization
Adding authentication and authorization to an API is important to ensure secure access and control over sensitive data. Here's how to go about it:
- Choose a method of authentication and authorization (e.g. JWT, OAuthz)
First, choose a method of authentication and authorization that fits the needs of the API. Common methods include JWT (JSON Web Tokens), OAuth2, and Basic Authentication.
- Implement authentication and authorization middleware
Next, you need to implement middleware functions to handle authentication and authorization. These functions should check for valid authentication credentials and authorize access to endpoints based on the user's role and permissions.
A basic JWT authentication middleware function:
func RequireTokenAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
token, err := jwt.Parse(authHeader, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte("secret"), nil
})
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
if !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
context.Set(r, "decoded", token.Claims)
next.ServeHTTP(w, r)
})
}
- Adding authentication and authorization to endpoints
After the middleware has been implemented, we can add it to desired endpoints. For example, to restrict access to a specific endpoint to authenticated users with a certain role, we can use the RequireTokenAuthentication
middleware and add a role check:
router.HandleFunc("/protected", RequireTokenAuthentication(ProtectedHandler)).Methods("GET")
Testing the API
To ensure the API is functioning correctly, we need to write tests. It's important to test it thoroughly to ensure it works as expected and to catch any bugs or issues before they reach users.
Testing can be divided into unit tests for individual handlers and endpoints, and integration tests for the API as a whole. Testing frameworks like GoConvey and Ginkgo can help with writing and running tests.
- Writing unit tests for endpoints and handlers.
Unit tests are focused on testing individual functions or components of the code. For our API, we'll want to write unit tests for each endpoint and handler function to ensure they are working properly.
Here's a test function for a handler:
func TestProtectedHandler(t *testing.T) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": "testuser",
"role": "admin",
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
tokenString, err := token.SignedString([]byte("secret"))
if err != nil {
t.Fatalf("Error signing token: %v", err)
}
req, err := http.NewRequest("GET", "/protected", nil)
if err != nil {
t.Fatalf("Error creating request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+tokenString)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ProtectedHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `{"message":"Hello, authenticated user!"}`
if rr.Body.String() != expected {
t.Errorf("Handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
}
- Writing integration tests for the API:
Integration tests, on the other hand, test the API as a whole to ensure all components are working together correctly.
An integration test using the Ginkgo testing framework:
var _ = Describe("API", func() {
var (
apiURL string
api *API
)
BeforeEach(func() {
apiURL = "http://localhost:8080"
api = NewAPI()
go api.Start()
time.Sleep(1 * time.Second)
})
AfterEach(func() {
api.Stop()
})
Describe("GET /users", func() {
It("returns a list of users", func() {
resp, err := http.Get(fmt.Sprintf("%s/users", apiURL))
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))
bodyBytes, err := ioutil.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
bodyString := string(bodyBytes)
Expect(bodyString).To(ContainSubstring("John Doe"))
Expect(bodyString).To(ContainSubstring("johndoe@example.com"))
})
})
})
Testing is an essential part of building a robust API, and using testing frameworks like GoConvey or Ginkgo can make the process easier and more effective.
Deploying the API
Great! Once our API is developed, tested, and ready for production, we need to deploy it so that our clients can access it. Depending on your project requirements and infrastructure capabilities, there are various deployment methods to choose from. (e.g., Docker, Heroku, AWS)
We will go through the steps for deploying our API on Heroku, a popular Platform as a Service (PaaS) provider.
Create a Heroku account:
First, create a Heroku account if you don't have one already. Visit the Heroku website and sign up for a free account.Install the Heroku CLI:
Next, download and install the Heroku CLI by following the instructions given on the Heroku website. This CLI will enable us to interact with Heroku from our terminal.Create a new Heroku app:
After you have the Heroku CLI installed, create a new Heroku app by running the following command in your terminal:
heroku create <app-name>
Replace <app-name>
with the name you want to give your app. This will create a new app on Heroku and give you a URL for accessing it.
- Set up environment variables: If your API uses any environment variables, such as database connection strings or authentication keys, you need to set them up on Heroku as well. You can do this by running the following command in your terminal:
heroku config:set <KEY>=<VALUE>
Replace <KEY>
with the name of your environment variable and <VALUE>
with its value.
Commit your code to Git:
Before we can deploy our app on Heroku, we need to commit our code to Git. Make sure that your code is in a Git repository and commit any changes you have made.Deploy your app:
When your code is committed to Git, you can deploy your app to Heroku by running the following command in your terminal:
git push heroku master
This will push your code to Heroku and trigger a build process. When the build is complete, your app will be up and running on Heroku.
- Scale your app: If you have a high traffic app, you can scale it up by running the following command in your terminal:
heroku ps:scale web=<number-of-instances>
Replace <number-of-instances>
with the number of instances you want to run.
Congratulations! You have successfully deployed your API on Heroku. You can now access it using the URL provided by Heroku.
Conclusion
In this tutorial, we have covered the basics of building APIs with Go. We started with understanding what APIs are and why they are useful. We then went on to explore the benefits of using Go for building APIs and set up our development environment.
We learned about choosing a framework and creating a basic server, adding routes and handlers for different endpoints, implementing CRUD operations, handling errors and exceptions, integrating with a database, and adding authentication and authorization. Finally, we covered testing the API and deploying it using Docker.
While we have covered a lot in this tutorial, there is still more to learn about building APIs with Go. To continue your learning journey, check out the additional resources listed below:
- Go Documentation
- Go Web Examples
- Create A Simple RESTful API With Golang
- Advanced Go Concurrency Patterns
Thank you for reading, and I hope you found this tutorial helpful. If you have any questions or comments, feel free to connect with me on LinkedIn or Twitter.
You can support me by buying me a coffee/book :)
Image by Freepik
Posted on April 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.