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.