Contents

    Writing REST APIs in Go

    Ukeje C. Goodness

    Ukeje C. Goodness is a tech explorer exploring backend development in Go and machine learning in Python while writing about those topics.

    Published on

    August 4, 2022
    Writing REST APIs in Go

    REST (representational state transfer) APIs are APIs that comply with the RESTful architectural standards. APIs that comply with the RESTful architectural standards have to be stateless, use a client-server architecture, be cacheable and layered.

    The Go programming language is one of the most preferred for writing backend applications and RESTful APIs due to its speed and other features, which include concurrency, readability, and ease of use.

    This tutorial will teach you how to build REST APIs in Go fast using the Fiber web framework, an SQLite database, and the GORM-ORM. You'll learn by building a simple blog API for hands-on experience.

    Prerequisites

    To follow this hands-on tutorial, you'll have to meet these requirements:

    • You have knowledge of working with Go.
    • You have Go 1.17 or newer installed on your computer.
    • You have SQLite installed on your computer.
    • Meeting these requirements will ensure a frictionless learning process as you follow this tutorial.

    Getting Started

    You'll need to install some external packages to follow this tutorial; the framework you'll be using for routing and the ORM you'll be using to interact with the SQLite database.

    Fiber logo

    The framework is Fiber. Fiber is an ExpressJS-inspired Go web framework built on the fasthttp engine to deliver speed, performance, and ease of use.

    Install the Fiber framework by running this command below in your Go workspace. Note that Fiber requires at least Go version 1.14 to run.

    go get github.com/gofiber/fiber/v2

    GORM is the most popular SQL object-relational mapping (ORM) package supporting numerous SQL database paradigms like MySQL, PostgreSQL, SQLite, and the Microsoft SQL Server.

    Install the latest version of GORM and the GORM SQLite driver using these commands.

    go get -u gorm.io/gorm
    go get -u gorm.io/driver/sqlite

    Once you have installed GORM and Fiber, you can now import them in a Go file in the workspace.

    import (
    	"github.com/gofiber/fiber/v2"
    	"gorm.io/driver/sqlite"
    	"gorm.io/gorm"
    )
    

    These packages are all the external dependencies you'll need to follow this tutorial and build REST APIs in Go fast.

    Next, you'll have to set up a database and a database connection for the REST API.

    Managing SQLite Database Migrations Using GORM

    GORM is an ORM, which means it keeps you away from the hassles of writing raw SQL; instead, you get to use native Go data structures to work with your database.

    First, you want to assign global variables. In the program, you'll need to handle a lot of errors; you'll also need to use the database in multiple requests; you can assign a database and error variable globally as thus:

    var (
    	DB  *gorm.DB
    	err error
    )
    

    err is of type error; all errors in the program would be of type err, and you won't have to declare new error variables all over.

    The variable DB is of the type pointer to gorm.DB, which is the database itself.

    Next, you have to define the columns in the database. GORM uses a struct type for this purpose, and you'll have to export the struct type and its fields by capitalizing the names since you'll be using and manipulating them in your program.

    type Blogs struct {
    	gorm.Model
    	Title  string `json:"name"`
    	Author string `json:"author"`
    	Reads  int    `json:"reads"`
    }
    

    The struct Blogs above has three fields and inherits the GORM Model gorm.Model. The fields were set to JSON fields that will be parsed from the API requests and then manipulated.

    gorm.Model is a GORM struct containing popularly used columns in databases such as the created time, deleted time, updated time, and ID. The ID is the primary key by default.

    a GORM struct containing popularly used columns in databases such as the created time, deleted time, updated time, and ID.

    Suppose you're not using the gorm.Model struct, you can choose to set a primary key by using gorm: "primaryKey" as the struct tags; However, note that gorm sets fields of ID as the primary key on default even without the struct tags set.

    Creating an SQLite database connection

    You'll have to connect to a database, migrate the struct model you just declared, and perform database operations. Here's how to connect to an SQLite database using GORM.

    DB, err = gorm.Open(sqlite.Open("Blogs.db"), &gorm.Config{})
    	if err != nil {
    		panic("failed to connect database")
    	}
    }
    

    The DB and err global variables are assigned to gorm.Open. gorm.Open takes in sqlite.Open where the database path is passed, and a reference to gorm.Config an interface containing GORM's configurations. Then, an error is anticipated to be handled for cases where there is a failure to connect to the database.

    In cases where a database isn't found, for SQLite databases, an in-memory database is created by the database driver for a successful connection.

    Next, you auto migrate the database. DB.AutoMigrate manages the database migration for the assigned model referenced as a parameter.

    	err = DB.AutoMigrate(&Blogs{})
    	if err != nil {
    		panic("migration failure")
    	}
    

    In this case, the model is the Blogs model assigned above, after which a migration error is handled.

    You might want to have everything pertaining to the database in a function that gets called in the main function as thus:

    How code should look after which a migration error is handled.

    Endpoint Routing using Fiber

    Now that you have set up the database connection and auto migrations using GORM, the next thing is to define the endpoints for your REST API using Fiber.

    A typical blog API would have CRUD(Create, Read, Update, Delete) functionalities representing the POST, GET, PUT and DELETE endpoints of the API.
    The fiber framework requires you to declare a pointer to the fiber.App interface to set endpoint routes using the instance you declared.

    func InitRouters(app *fiber.App) {
    	app.Get("/api/v1/blog/:id", GetBlog)
    	app.Post("/api/v1/blog", CreateBlog)
    	app.Put("api/v1/blog/:id", UpdateBlog)
    	app.Delete("/api/v1/blog/:id", DeleteBlog)
    }
    
    

    Declaring a function to handle all your routes is advised as it's more efficient, and you can define all routes in the function which you'll call in the main function.

    In the InitRouters function, the endpoints of the REST APIs are declared.

    You can declare endpoints using the methods of app; the instance of  Fiber.App. To declare an endpoint, you use methods like Get, Post, Put and Delete; these methods take in the endpoint and a handler function as shown above.

    Creating Handler Functions for Request Types

    In the handler functions, you might want to format messages, like errors and successful request messages. You can create a message struct and use the encoding/json package to convert it into JSON for your messages.

    type Message struct {
    	Status      string
    	Description string
    }
    

    The Status field will be initialized with success or error while the Description field will vary based on the occurrences.

    Implementing the POST Requests Handlers

    Conventionally, POST requests and POST request handlers are used to receive inputs from users calling the APIs.

    Creating handler functions using the Fiber framework requires that the function takes in a pointer to the fiber context *fiber.Ctx and returns an error.

    The POST request of this Blog API would receive inputs (new blogs) from the user and migrate it into the database if there are no errors.

    func CreateBlog(ctx *fiber.Ctx) error {
    	blog := new(Blogs)
    	if err := ctx.BodyParser(blog); err != nil {
    		return ctx.Status(503).JSON(err)
    	}
    	DB.Create(&blog)
    	return ctx.Status(fiber.StatusOK).JSON(blog)
    }
    
    

    In the function body, a new Blogs type was instantiated. The POST request from the API user is then parsed into the new instance blog in the second line using ctx.BodyParser and a format error are handled.

    ctx.BodyParser parses the POST request body into a struct for manipulation and ease of input into the database. if there's an error, the 503 error code is sent to the user as a response along with the error.

    If parsing the request body was successful, you can insert the instantiated struct model of the same type into the database using the Create method of your database instance.

    In this case, the request body is parsed into the blog variable of the Blog type, and the parsed blog is inserted into the database as a new row entry.

    Ctx.Status is a fiber error type that returns a status code and optional JSON. In this case, the entry was returned as a response; you can also return custom messages and you'll learn how to later in this tutorial.

    Implementing the GET Request Handlers

    GET requests are used for data retrievals. A GET request handler retrieves data from the database and makes it available to the API as a response when the endpoint is called.

    In the case of the Blog API, the GET request would retrieve a blog row from the database and format it into JSON for the response.

    The id parameter of the GET request is what's important and will be used to retrieve data. Remember the id is generated by GORM as part of the GORM model. The id can be retrieved from the request body using ctx.Params which is the method for retrieving parameters in Fiber.

    func GetBlog(ctx *fiber.Ctx) error {
    	id := ctx.Params("id")
    	var blog Blogs
    	result := DB.Find(&blog, id)
    	if result.RowsAffected == 0 {
    		return ctx.SendStatus(404)
    	} else {
    	return ctx.Status(fiber.StatusOK).JSON(&blog)
    } 
    }
    

    The blog variable of type Blogs struct will be parsed as a JSON response.

    Since we have to search the database for the ID, GORM provides many database methods for searching a database, such as the First and Find methods.

    The Find method returns all entries, and the First method returns the first entry that matches the query on the database.

    The two methods take in a struct type and the query parameter, and when a match is found, the row is parsed into a struct for easy manipulation. In this case, the result variable is declared to hold the struct returned by the Find method.

    The if statement checks if any rows were affected. If no rows were affected, the data was not found, and you have to return a 404 error (Not Found error) using ctx.SendStatus; the method for sending status code using fiber.

    If any rows were affected, it's safe to conclude that the data was found, and you can return a response as seen in the else statement where the blog struct was parsed into JSON and returned using ctx.Status which allows for sending a status code and a message.

    Implementing the PUT Requests Handlers

    PUT requests are used to update entries. PUT requests are similar to POST requests except at the database level.

    For this Blog API, the PUT request will update a blog row using the blog id inserted through the GORM Model.

    GORM provides the Where method for a thorough search through a database after which you use a finisher method. Finisher methods like Update, Save, and Delete can be used on the Where method for updating, saving and deleting entries respectively.

    func UpdateBlog(ctx *fiber.Ctx) error {
    	blog := new(Blogs)
    	id := ctx.Params("id")
    	if err := ctx.BodyParser(blog); err != nil {
    		return ctx.Status(503).JSON(Message{
    			Status:      "Error",
    			Description: "There was an error parsing your request",
    		})
    	}
    	DB.Where("id = ?", id).Updates(&blog)
    	return ctx.Status(200).JSON(blog)
    }
    

    First, a new Blogs type is created, and the parameter id is retrieved from the request body and the parsed parameter body is parsed using ctx.BodyParser, after which an error is returned if there's a parser error. The id parameter will be used to search through the database.

    The error message returned is a customized JSON. The Message is initialized and parsed into JSON using the JSON method of ctx.Status.

    On successful parsing and id retrieval, DB.Where is used to search the database using the id as shown above, and the new Blog type blog is inserted to update the row using the Update method by passing a reference to the Blog type &blog.

    Just as in the POST request, you could choose to handle errors by checking affected rows and returning a not found error if there are no rows affected. The PUT request handler UpdateBlog returns a status code 200 (successful) and a JSON of the updated row.

    Implementing the DELETE Request Handler

    DELETE request endpoints and handlers are used to delete entries from a database, and in this Blog API, the delete request handler would delete a blog row from the database.

    Like the UPDATE request handler, you could use the Where method to search through the database and delete a row using the Delete finisher method, or you could simply use the Delete method of the database instance.

    func DeleteBlog(ctx *fiber.Ctx) error {
    	id := ctx.Params("id")
    	var blog Blogs
    	search := DB.Delete(&blog, id)
    	if search.RowsAffected == 0 {
    		return ctx.Status(404).JSON(Message{
    			Status:      "Error",
    			Description: "404: Not Found",
    		})
    	}
    	return ctx.Status(200).JSON(Message{
    		Status:      "Success",
    		Description: "The post was successfully deleted",
    	})
    }
    

    In the DeleteBlog function, the id is retrieved from the request body, and a Blog type blog is created.

    Using DB.Delete, passing in reference to the created Blog type and the id as parameters; you can delete a row entry.

    Just as in the POST request handler, in the function, rows affected were checked using an if statement. In the DeleteBlog function, the errors are customized using the message type and returned using the JSON method of ctx.Status.

    Conclusion

    Building REST APIs in Go is easy and intuitive, as seen in this tutorial. This tutorial taught you how to build RESTful APIs in Go fast with the Fiber framework and the GORM SQL ORM using an SQLite database by building a simple Blog API.

    You also learned how to handle and customize errors while building APIs in Go and the different ways you could use GORM to work with a database, using methods like finisher methods to perform CRUD operations.

    You can find the complete program of this tutorial here.

    RESTful APIs are among the most popular ways of interacting with server-side architecture. Since building APIs is one of the jobs of a backend developer/engineer, the knowledge you've gained from this tutorial would go a long way.

    Data-rich bug reports loved by everyone

    Get visual proof, steps to reproduce and technical logs with one click

    Make bug reporting 50% faster and 100% less painful

    Rating LogosStars
    4.6
    |
    Category leader

    Liked the article? Spread the word

    Put your knowledge to practice

    Try Bird on your next bug - you’ll love it

    “Game changer”

    Julie, Head of QA

    star-ratingstar-ratingstar-ratingstar-ratingstar-rating

    Overall rating: 4.7/5

    Try Bird later, from your desktop