DEV Community

Cover image for Handling Dynamic API Response In Go
Adetoye Anointing
Adetoye Anointing

Posted on

Handling Dynamic API Response In Go

A Go developer working on a service that requires a third-party API integration and that integration has a webhook or something similar that returns a changing response body structure depending on the event sent to and through the hook.

A lot of people do not essentially know how to handle this scenario in Go at least using the gracious and glorious JSON package to handle something simple yet overly complicated task in Go.

Today, we will have a walkthrough on handling dynamic API responses in Go using the JSON package.

Prerequisite

It is crucial to understand that dynamic API responses take different forms, some are events triggered in response to an activity in the service. Whether it is a FinTech API using a webhook to notify the system of an event or a user triggering a notification to an admin dashboard on a specific event.

These dynamic APIs hold a means of identification that can be used to identify them and separate them into their different predefined structure in most cases.

let's get into the fun part of this article [the coding part]

To get started with this tutorial, you will need:

  • Go installed and set up
  • An IDE or test editor
  • Practical Knowledge of Go
  • Postman

Initial stage [ Setup your project repo ]

Open the terminal and set up our project repo using the command below. (This of course works on Linux and macOS)

mkdir Documents/dynamic-API && touch Documents/dynamic-API/
main.go && code Documents/dynamic-API
Enter fullscreen mode Exit fullscreen mode
go mod init [your-github-name]/handling-dynamic-api
Enter fullscreen mode Exit fullscreen mode

Create our main function and setup for development.

package main

import "log/slog"


func main() {
    // setup a logger using slog
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    logger.Info("Hello Terminal πŸ‘‹", "user", os.Getenv("USER"))

}
Enter fullscreen mode Exit fullscreen mode

Build the function that handles the API response for the service.

func HandleDynamicAPI(l *slog.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        l.Info("This API is connected", "user", os.Getenv("USER"))
    }
}
Enter fullscreen mode Exit fullscreen mode

This function will hold the entire logic that handles the response from the webhook.
Since we are handling this without a third-party library or framework, we will be using the built-in Go http library to handle this bit which will require importing net/http while also defining a logger of type *slog.Logger as an argument to the HandleDynamicAPI function

random fun fact: if you are not a VIM / Nano user, you might not need to worry about importation cause your text editor probably have it handled

Hence; your current code should look something like this

package main

import (
    "log/slog"
    "net/http"
)


func main() {
    // setup a logger using slog
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    logger.Info("Hello Terminal πŸ‘‹", "user", os.Getenv("USER"))

}

func HandleDynamicAPI(l *slog.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        l.Info("This API is connected", "user", os.Getenv("USER"))

    }
}
Enter fullscreen mode Exit fullscreen mode

Catch And Identify The Response Event Or Identifier (in case of an API with events or identifiers)

  • Event Identifier Structure
//this will be used to identify the event type
//
//We care about just the event head
type eventIdentfier struct {
    Event string `json:"event"`
}
Enter fullscreen mode Exit fullscreen mode
  • Logic To Catch And Identify Event
func HandleDynamicAPI(l *slog.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        l.Info("This API is connected", "user", os.Getenv("USER"))

        var (
            eventIdentfier    eventIdentfier
            jsonData          json.RawMessage
        )

        if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil {
            l.Error("error decoding json response", "error context", err)
            return
        }

        if err := json.Unmarshal(jsonData, &eventIdentfier); err != nil {
            l.Error("error unmarshalling json data message", "error context", err)
            return
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What the written code above does is it catches the API response body data and decodes it into the jsonData variable which is a type of json.RawMessage and that jsonData value is unmarshalled into the eventIdentifier variable to be used in the later stage of development.

How the json.RawMessage works are it delays json decoding of the body making it flexible to use for re-unmarshalling as I will call it and in our case used for event identification and eventually decoding into a defined json structure.

Using Event To Identify Execution Case

In this part, we will be going deeper into Go using control flow to execute logic and many cases functions after identification of the event that should be executed.

firstly, we want to know what the event value will be, which will be used to handle the control flow of the program.
These are usually found in the API / webhook documentation and our case we are using paystack.

For information about Paystack's webhooks, check out their API documentation and visit Paystack Webhook URL.

The event we will simulate for this tutorial is :

  • Payment Request Pending
  • Payment Request Successful

Looking at the response structure provided by paystack we can easily create a struct and fill out the fields needed but in our case due to some reason unknown to me for the fun of it; I will be using the entire field :).

Response Body Data Structure For Unmarshalling

type paymentPending struct {
    Event string `json:"event"`
    Data  struct {
        ID               int       `json:"id"`
        Domain           string    `json:"domain"`
        Amount           int       `json:"amount"`
        Currency         string    `json:"currency"`
        DueDate          any       `json:"due_date"`
        HasInvoice       bool      `json:"has_invoice"`
        InvoiceNumber    any       `json:"invoice_number"`
        Description      string    `json:"description"`
        PdfURL           any       `json:"pdf_url"`
        LineItems        []any     `json:"line_items"`
        Tax              []any     `json:"tax"`
        RequestCode      string    `json:"request_code"`
        Status           string    `json:"status"`
        Paid             bool      `json:"paid"`
        PaidAt           any       `json:"paid_at"`
        Metadata         any       `json:"metadata"`
        Notifications    []any     `json:"notifications"`
        OfflineReference string    `json:"offline_reference"`
        Customer         int       `json:"customer"`
        CreatedAt        time.Time `json:"created_at"`
    } `json:"data"`
}

type paymentSuccessful struct {
    Event string `json:"event"`
    Data  struct {
        ID            int       `json:"id"`
        Domain        string    `json:"domain"`
        Amount        int       `json:"amount"`
        Currency      string    `json:"currency"`
        DueDate       any       `json:"due_date"`
        HasInvoice    bool      `json:"has_invoice"`
        InvoiceNumber any       `json:"invoice_number"`
        Description   string    `json:"description"`
        PdfURL        any       `json:"pdf_url"`
        LineItems     []any     `json:"line_items"`
        Tax           []any     `json:"tax"`
        RequestCode   string    `json:"request_code"`
        Status        string    `json:"status"`
        Paid          bool      `json:"paid"`
        PaidAt        time.Time `json:"paid_at"`
        Metadata      any       `json:"metadata"`
        Notifications []struct {
            SentAt  time.Time `json:"sent_at"`
            Channel string    `json:"channel"`
        } `json:"notifications"`
        OfflineReference string    `json:"offline_reference"`
        Customer         int       `json:"customer"`
        CreatedAt        time.Time `json:"created_at"`
    } `json:"data"`
}
Enter fullscreen mode Exit fullscreen mode

declare a variable of both defined structures in the existing variable list

paymentPending paymentPending
paymentSuccessful paymentSuccessful

After defining the struct and declaring the variables, we will then proceed to write out the control flow and implement the logic for the program

        switch eventIdentfier.Event {
        case "paymentrequest.pending":
            l.Info("payment pending hook event", "response event title", eventIdentfier.Event)

            if err := json.Unmarshal(jsonData, &paymentPending); err != nil {
                l.Error("error marshalling pending payment data", "error context", err)
                return
            }

            if err := json.NewEncoder(w).Encode(map[string]any{"event type": paymentPending.Event, "amount": paymentPending.Data.Amount}); err != nil {
                l.Error("error encoding data to send as response", "error context", err)
                return
            }
            w.Header().Add("Content-Type", "application/json")

            l.Info("pending response data unmarshalled successfully", "pending ID", paymentPending.Data.ID, "pending amount", paymentPending.Data.Amount)
        case "paymentrequest.success":
            l.Info("payment successful hook event", "response event title", eventIdentfier.Event)

            if err := json.Unmarshal(jsonData, &paymentSuccessful); err != nil {
                l.Error("error marshalling successful payment data", "error context", err)
                return
            }

            if err := json.NewEncoder(w).Encode(map[string]string{"event type": paymentSuccessful.Event}); err != nil {
                return
            }
            w.Header().Add("Content-Type", "application/json")

            l.Info("success response data unmarshalled successfully", "success ID", paymentSuccessful.Data.ID, "success amount", paymentSuccessful.Data.Amount)
        default:
            l.Info("no event type found", "response event title", eventIdentfier.Event)
            w.WriteHeader(http.StatusInternalServerError)
        }
Enter fullscreen mode Exit fullscreen mode

The code above retrieves the event from the variable named eventIdentifier. It then uses the switch control flow to determine which API event has been passed to the endpoint. After identifying the API event, the data is decoded into the defined struct. Finally, an API response is returned with the required specified data.

There are instances where data is handled differently, and it doesn't involve returning a few selected fields as shown in the example above. Such cases include:

  • Storing the data in a database
  • Using the data for manipulation purposes
  • Utilizing the data for verifying a triggered action, and so on.

focusing on the basics is crucial for building a strong foundation to tackle more advanced projects.

Plug In The Function To An Endpoint

Plugging a function into an endpoint is easily one of the first you learn while studying Go and I am counting that this won't be the tricky part.

  • Plug in the function to the http handler using this line

http.HandleFunc("/dynamic-hook", HandleDynamicAPI(logger))

  • setup service listener

go log.Fatal(http.ListenAndServe(":3000", nil))

These should be done in the main function, hence; the main function should look like this

func main() {
    // setup a logger using slog
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    logger.Info("Hello Terminal πŸ‘‹", "user", os.Getenv("USER"))

    http.HandleFunc("/dynamic-hook", HandleDynamicAPI(logger))

    go log.Fatal(http.ListenAndServe(":3000", nil))
}
Enter fullscreen mode Exit fullscreen mode

Running, Testing And Finishing

  • Run Go Server

We run the program with our gracious Go command in the terminal:

go run main.go
Enter fullscreen mode Exit fullscreen mode
  • Test Endpoint And Simulate Webhook

To do this we have to copy the webhook json data and pass it to the designated endpoint using Postman

  • paymentrequest.success event response
    paymentrequest.success event response

  • paymentrequest.pending event response
    paymentrequest.pending event response

Conclusion

You should have a basic understanding of how to handle dynamic API response bodies, including use cases and the importance of the json package in Go for handling responses.

I hope you found this tutorial helpful.

Have fun practicing :)

Link To The Entire Code Sample

Handling Dynamic API Response

Top comments (2)

Collapse
 
benbpyle profile image
Benjamen Pyle

Nice article!

Collapse
 
yorubad-dev profile image
Adetoye Anointing

thank you very much