How to build an API with Go and GoFr

How to build an API with Go and GoFr

In this guide, we’ll build an order management API using Go and GoFr.
If you’re unfamiliar with API terminology, the acronym CRUD might seem a bit blunt. However, it’s a cornerstone concept! CRUD stands for Create, Read, Update, and Delete, representing the fundamental operations necessary for persistent storage. This model underpins much of the structure of the internet.

GoFr is an opinionated framework designed to streamline micro-service development in Go. It prioritises speed and boasts built-in features for essential functionalities like observability and security. This, coupled with its focus on clear conventions, makes GoFr a compelling choice for developers, particularly those new to micro-services.

By the article’s end, you’ll wield a robust API for seamless orders management, expertly crafted with Go and GoFr.

Setting up Go development environment

Get ready to code in Go! Setting up your development environment is a breeze, even if you’re new to the language.

Installing Go

You can download Go through the official page. Once, installed you can verify version by running the following command in your terminal:

go version

It should give you output of the below format:

go version go1.22.0 darwin/arm64

Setting up GoFr

Since, we’ll be using GoFr framework for this project, you would need to download it from Go’s built-in package manager.

  1. Create a project directory:
mkdir gofr-sample-app

2. Initialise Go project:

go mod init gofr-sample-app

3. Adding gofr as dependency:

go get gofr.dev

Designing the Order model

Before we explore the functionalities of our order management API, let’s establish the building block: the order model. This model represents each order details digitally, similar to how you might write them down on paper.

Our Order model will be containing 5 attributes: ID, Customer_ID, Products, Status, Created_At. To make this project more informative, we will be using a SQL database for storing the orders.

First, create a directory models in the projects root. In this we will create a order.go file and write the model:

type Order struct {
    ID         uuid.UUID `json:"id"`
    CustomerID uuid.UUID `json:"customer_id"`
    Products   []Product `json:"products"`
    Status     string    `json:"status"`
    CreatedAt  string    `json:"-"`
    UpdatedAt  string    `json:"-"`
    DeletedAt  string    `json:"-"`
}

You might have noticed that we have used a custom type Product , so lets go ahead and define that as well in a new file named product.go in models directory.

type Product struct {
    ID    uuid.UUID `json:"id"`
    Name  string    `json:"name"`
    Price int       `json:"price"`
}

Adding Routes and Connecting to DB

Now, that we have defined models for the project, we can go ahead with connecting to DB, adding routes and handling data.

  1. Create a file main.go in root of your project, which will serve as a entry point for incoming requests. Here’s what we will start with:
func main() {
    // Create a new application
    app := gofr.New()

    // Add required routes
    app.POST("/orders", handlers.Create)
    app.GET("/orders", handlers.GetAll)
    app.GET("/orders/{id}", handlers.GetByID)
    app.PUT("/orders/{id}", handlers.Update)
    app.DELETE("/orders/{id}", handlers.Delete)

    app.Run()
}

This file imports a handlers directory, which we will write later. We have defined the routes, now lets connect to DB.

2. Create a new directory configs, and within that create a file .env which will store all the environment configs for the project. Add the below lines to the file:

DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=root123
DB_NAME=orders
DB_PORT=2006
DB_DIALECT=postgres

GoFr will make a DB connection with the provided configs. Make sure you have a docker instance running in your local for postgres, if not you can start it with the below command:

docker run --name gofr-pgsql -d -e POSTGRES_DB=orders -e POSTGRES_PASSWORD=root123 -p 2006:5432 postgres:15.3

Building CRUD handlers

Before starting with the code, first create a new directory handlers and a new file orders.go within it.

  • **CREATE
    **Lets start with creating a new order. Adding new items is done through a POST request to the /orders endpoint. This follows the popular REST design pattern. But before we add the item to our database, we need to make sure the information provided is valid. This includes checking for missing fields and ensuring the data types are correct.
    Now, add the below lines to the handlers/orders.go file:
func Create(ctx *gofr.Context) (interface{}, error) {
  var order model.Order

  err := ctx.Bind(&order)
  if err != nil {
      return nil, errors.New("invalid param: body")
  }

  id, err := uuid.NewUUID()
  if err != nil {
      return nil, errors.New("error in creating uuid")
  }

  createdAt := time.Now().UTC().Format(time.RFC3339)

  _, err = ctx.SQL.ExecContext(ctx, "INSERT INTO orders (id, cust_id, products, status, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6)",
      id, order.CustomerID, pq.Array(order.Products), order.Status, createdAt, createdAt)
  if err != nil {
      return nil, err
  }

  order.ID = id

  return order, nil
}
  • **READ
    **Just storing info about orders isn’t sufficient if we can’t retrive it. So, lets define you read handlers.
    i) List (or Read All): This will be done through a GET request to the /orders endpoint.
    ii) GetByID: This will be done through a GET request to the /orders/{id} endpoint.
    Now, that we are clear on the read routes, let add the handlers for them. Append the below code to the handlers/orders.go file:
func GetAll(ctx *gofr.Context) (interface{}, error) {
    rows, err := ctx.SQL.QueryContext(ctx, "SELECT id, cust_id, products, status FROM orders WHERE deleted_at IS NULL")
    if err != nil {
       return nil, err
    }

    defer rows.Close()

    var orders []model.Order

    for rows.Next() {
       var o model.Order

       err = rows.Scan(&o.ID, &o.CustomerID, pq.Array(&o.Products), &o.Status)
       if err != nil {
          return nil, err
       }

       orders = append(orders, o)
    }

    err = rows.Err()
    if err != nil {
       return nil, err
    }

    return orders, nil
}
func GetByID(ctx *gofr.Context) (interface{}, error) {
    id := ctx.PathParam("id")
    if id == "" {
        return nil, errors.New("missing param: ID")
    }

    uid, err := uuid.Parse(id)
    if err != nil {
        return nil, errors.New("invalid param: ID")
    }

    var order model.Order

    err = ctx.SQL.QueryRowContext(ctx, "SELECT id, cust_id, products, status FROM orders WHERE id=$1 and deleted_at IS NULL", uid).
      Scan(&order.ID, &order.CustomerID, pq.Array(&order.Products), &order.Status)
    if err != nil {
        return nil, err
    }

    return &order, nil
}
  • **UPDATE
    **There would also be a scenario where we would need to update the order. Here, we would need to consider that we are updating the specified order only. So, this will be done by PUT request to the /orders/{id} endpoint. This will update the order of specified ID with the given input fields.
func Update(ctx *gofr.Context) (interface{}, error) {
    var order model.Order

    id := ctx.PathParam("id")
    if strings.TrimSpace(id) == "" {
       return nil, errors.New("missing param: ID")
    }

    uid, err := uuid.Parse(id)
    if err != nil {
       return nil, errors.New("invalid param: ID")
    }

    order.ID = uid

    err = ctx.Bind(&order)
    if err != nil {
       return nil, errors.New("invalid param: body")
    }

    updatedAt := time.Now().UTC().Format(time.RFC3339)

    res, err := ctx.SQL.ExecContext(ctx, "UPDATE orders SET cust_id=$1, products=$2, status=$3, updated_at=$4 WHERE id=$5 and deleted_at IS NULL",
       order.CustomerID, pq.Array(order.Products), order.Status, updatedAt, order.ID)
    if err != nil {
       return nil, err
    }

    rowsAffected, _ := res.RowsAffected()

    if rowsAffected == 0 {
       return nil, errors.New("entity not found")
    }

    return order, nil
}
  • **DELETE
    **To fulfil the requirement of deletion, we would need to make a DELETE request to /orders/{id} endpoint. This will mark the order with specified ID as deleted.
func Delete(ctx *gofr.Context) (interface{}, error) {
    id := ctx.PathParam("id")
    if id == "" {
       return nil, errors.New("missing param: ID")
    }

    uid, err := uuid.Parse(id)
    if err != nil {
       return nil, errors.New("invalid param: ID")
    }

    deletedAt := time.Now().UTC().Format(time.RFC3339)
    updatedAt := deletedAt

    res, err := ctx.SQL.ExecContext(ctx, "UPDATE orders SET deleted_at=$1, updated_at=$2 WHERE id=$3 and deleted_at IS NULL", deletedAt, updatedAt, uid)
    if err != nil {
       return nil, err
    }

    rowsAffected, _ := res.RowsAffected()

    if rowsAffected == 0 {
       return nil, err
    }

    return nil, nil
}

RESULT

Now, we are done with the all the CRUD handlers, lets try running the application. You can do this by running the below command from the root of project.

go run main.go

This will compile and start your application, and if everything is configured correctly then you would see a response like this:

INFO [11:31:19] Loaded config from file: ./configs/.env
INFO [11:31:19] connected to 'orders' database at localhost:2006
INFO [11:31:19] Starting metrics server on port: 2121
INFO [11:31:19] Starting server on port: 8000

Now, you can hit requests to the server using any browser, or tools like Postman, Insomnia…
You can also check the GoFr’s default metrics on localhost:2121/metrics.

CONCLUSION

This article guided you through building a performant CRUD API for orders management using Go and GoFr. Though focused, it tackles a practical problem.

Ready to take it further? Consider enhancing error validation, and even writing unit tests.

Want to know how to run schema migrations for your database?
You can find a detailed explanation in my other article.

**Want to explore or enhance this project on order management?
**You are welcome to check out the GitHub repo.

Happy Coding 🚀

Thank you for reading until the end. Before you go:

  • Please consider liking and following! 👏

  • Follow me on: LinkedIn | Medium

Did you find this article valuable?

Support Unravelling Go with Srijan by becoming a sponsor. Any amount is appreciated!