How to Consume PayStack APIs in a NodeJs/Typescript backend

There has been a lot of talk about typescript and the benefit it provides from what javascript has to offer.

Well, Typescript is a superset of Javascript, which means whatever feature Javascript has, Typescript has it.

Typescript gives us type safety and allows us to define the type of data we are collecting in a function, variable, method, or return function data

In this article, we are going to see where typescript shines when it comes to consuming external APIs.

When we consume external APIs, from the documentation, we will see what is expected of us to pass in the payload and response data upon a successful or fail return from the request.

In Javascript, you will have to consume everything from the response, but with typescript, you can use object destructing to pull out only the data you need and leave out the remaining response.

Implementation of PayStack Apis using Typescript

We are going to see where these come into play when we consume the Paystack APIs service.

We are going to consume three endpoints from the PayStack documentation

  • Get bank lists

  • Resolve account Name

  • Initialize payment

The thing we will need

  • Paystack docs

  • Paystack secret key (don’t share this with anyone)

  • Paystack base URL

  • install Axios package

To start, we need to create our DTOs (data transfer objects) interface, which defines how we exchange data between the different functions.

An interface allows a new type to be created with a name and structure. The structure includes all the properties and methods that the type has without any implementation.

— IPaystackBank response DTO

The interface defines the data we are collecting upon successfully fetching of bank list from a particular country.

export interface IPaystackBank {
    name: string,
    code: string,
    active: boolean,
    country: string,
    currency: string,
    type: string
  }

From the docs, you can collect more base on the response we got.

Account Name Inquiry Request DTO

We define an interface to accept the bank code and account number as a string.

export interface IAccountNameEnqury {
    bankCode: string, 
    accountNumber: string
}

— IPaystackResolveAccount Response DTO

The interface specifies what we are collecting upon successful account name resolution from paystack via the object destructuring.

export interface IPaystackResolveAccount {
    account_name: string
}

— Paystack initialize transactions Request DTO

export interface IPaymentInitializeRequest  {
    amountMajor: number,
    payingUser: IUser
}

IUser Interface

export interface IUser {
    firstName?: string,
    lastName?: string,
    email: string
}

The interface defines the kind of data, initialize Transaction function will accept where ever we call it in your project.

IUser interface defines email as required while the first name and last name can be nullable.

— Paystack initialize Transaction response DTO

We create an interface that defines the type of data we will fetch when we run object destructuring after a successful response from the initialized Transaction endpoint.

export interface IPaymentInitializeResponse {
    paymentProviderRedirectUrl: string
    paymentReference: string,
    accessCode: string
}

For clear separation of concern, create a payment service that will hold all our API calls to Paystack, there is no hard rule here, you can define your function and call the APIs anywhere in your code.

We will start by importing all our required functions

import { IPaystackBank } from "../dto/IPaystackBank"
import {IPaystackResolveAccount } from "../dto/IPaystackResolveAccount";
import axios, { AxiosResponse } from "axios"
import { IUser } from "../dto/IUser";
import { IPaymentInitializeResponse } from "../dto/IPaymentInitializepresonse";
import { IPaymentInitializeRequest } from "../dto/IPaymentInitializeRequest";
import { IAccountNameEnqury } from "../dto/IAccountNameEnqury";
import * as Util from "./helper"

Bank List

Next, we will create our first function which fetches lists of banks based on the country we provide via the function argument and return data according to the IPaystackBank interface definition

export const getBankLists = async (country: string): Promise<IPaystackBank[]> => {
    const baseUrl = process.env.PAYSTACK_BASE_URL + `/bank/?country=${country}`
    const headers = {
        Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
        'content-type': 'application/json',
        'cache-control': 'no-cache'
    }
   try {
       const response: AxiosResponse<any> = await axios.get(baseUrl, {headers})
       if( !response.data && response.status !== 200 ){
        throw new Error('An error occurred with our third party. Please try again at a later time.')
       } 
     const paystackBanks: IPaystackBank[] = response.data.data 
     return paystackBanks 
   } catch (e) {
    throw new Error('An error occurred with our third party. Please try again at a later time.')
   } 

}

getBankLists is an async function that returns a list of banks upon promise resolve, we use Try / catch to handle any error that is returned from the request and throw an error exception with a human-friendly message.

baseUrl is where set our base URL as specified in the docs definition.

The headers are where we set our authorization details(remember to keep your Paystack secret key safe).

The paystackBanks constant returns bank details in an array form define by IPaystackBank interface.

Name Inquiry

The whole essence of getting a list of banks is to be able to resolve account names based on the bank code and account number we provide.

export const accountNmeInqury = async (requestBody:IAccountNameEnqury): Promise<IPaystackResolveAccount> => {
    const {bankCode, accountNumber} = requestBody
    const baseURL =  process.env.PAYSTACK_BASE_URL + `/bank/resolve?account_number=${accountNumber}&bank_code=${bankCode}`
  const headers = {
    Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
    'content-type': 'application/json',
    'cache-control': 'no-cache'
  }
  try{
    const response: AxiosResponse<any> = await axios.get(baseURL, { headers })
    if(!response.data && response.status !== 200){
        throw new Error('An error occurred with our third party. Please try again at a later time.')
    }
    const payStackResolveAccount : IPaystackResolveAccount = response.data.data
    return payStackResolveAccount
  }catch(e){
    throw new Error('An error occurred with our third party. Please try again at a later time.')
  }
}

it's an async function that we use to try and catch to resolve the account name and throw an error exception upon failed request to paystack.

Our payStackResolveAccount constant return data according to the IPaystackResolveAccount interface definition.

Initialize Transaction

With this function, we can initialize payment transactions with a specific amount with the details of the user trying to make the payment.

what we get is a range of data but we are specifically interested in payment authorization URL, payment reference, and access code which is what we define in our IPaymentInitializeResponse interface, you can look at the docs and find more data you might want to collect from the response

export const initializeTransaction = async (requestData: IPaymentInitializeRequest): Promise<IPaymentInitializeResponse> => {
    const { payingUser, amountMajor} = requestData
    const base_url = process.env.PAYSTACK_BASE_URL + `/transaction/initialize`
    const headers = {
      Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      'content-type': 'application/json',
      'cache-control': 'no-cache'
    }
    const amountMinor = (amountMajor || 0) * 100
    try{
      const transactionFeeMajor = Util.getPaystackTransactionFeeMajor(amountMajor)
      const payload: any = {
        amount: ((amountMajor || 0) * 100) + (transactionFeeMajor * 100),
        email: payingUser.email,
        metadata: {
          full_name: `${payingUser.firstName} ${payingUser.lastName}`
        }
      }

      const response: AxiosResponse<any> = await axios.post(base_url, payload, {
        headers
      })

      if(!response.data && response.status !== 200){
        throw new Error('An error occurred with our third party. Please try again at a later time.')
    }
      const { authorization_url, reference, access_code } = response.data.data
      return {
        paymentProviderRedirectUrl: authorization_url,
        paymentReference: reference,
        accessCode: access_code
      }
    }catch(e){
      console.log(`e message`, e.message)
      console.log(e.stack)
      throw new Error('An error occurred with our payment provider. Please try again at a later time.')
    }
}

we use a helper function [getPaystackTransactionFeeMajor] to get the paystack transaction fee,

export const getPaystackTransactionFeeMajor = (amonutmajor: number) => {
        let transferFee = (0.015 * amonutmajor)
        if(amonutmajor > 2500){
            transferFee += 100
        }
        return transferFee > 2000 ? 2000 : transferFee
}

every amount is converted to kobo as you can see from the amount minor calculation.

Aside from the amount and payee email, we can pass other data inside our metadata, metadata is an optional field for the request payload as defined in the documentation

Conclusion

As we can see from our request to paystack, we are throwing an exception error once we can confirm the response we are getting from paystack does not contain data and the response status code is not 200.

We utilize the typescript interface feature to define the type of data we want to collect from each of the successful responses we get from paystack,

thus ensuring the type safety and making sure we only pull out the data we need.

You can find the full code on GitHub