DEV Community

Kizito Nwaka
Kizito Nwaka

Posted on

Creating a Simple Payment System in Node.js and MongoDb Using Paystack: A Step-by-Step Guide πŸ’³πŸ’Έ

Introduction

Hey folks! I am Kizito a Software Engineer at Microsofzz(wakes up). In this article I will be guiding you through developing API endpoints for payment integration on Paystack 😎 which can be used for your client side applications.

If you seek to monetize your applications with Paystack using Node.js seek no further, for you are in the right place 🀩

Prerequisites

  • Basic Knowledge of Javascript, Node.js and Express
  • You should have VS Code, Node.js, MongoDb and Postman Installed.
  • Experience in working with Databases (I'll be using MongoDb for this article).
  • A heart willing to learn ❀️‍πŸ”₯

PaystackPaymentJoy

Let's dive in

In today's digital age, where e-commerce is booming and online transactions have become the norm, building a seamless and secure payment system is paramount for businesses and developers alike. But building a payment system can be a daunting task, and that's where Paystack comes into play. Paystack, a widely trusted payment gateway, provides developers with the tools they need to handle payments effortlessly. In this comprehensive guide, we'll embark on a journey to demystify the process of building a cutting-edge payment system in Node.js using Paystack. So, fasten your seatbelts, as we embark on this exciting journey into the world of Node.js and Paystack-powered payment systems.

Setup

If you do not have a paystack account you'll need to start by creating one here https://dashboard.paystack.com/#/signup if you already have one then you can proceed to login.
Having done that we can proceed to development πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

I will be making use of couple of the most popular endpoints in this article which will be the create charge endpoints and the subscription endpoints πŸ₯πŸ₯

Open up your VS Code and create a new folder directly from your terminal as a senior dev 🫑. I'll be calling mine paystack-nodejs-integration

Then you cd into that directory using

mkdir paystack-nodejs-integration
cd paystack-nodejs-integration
Enter fullscreen mode Exit fullscreen mode

terminal

Now that we are in that same directory we do

npm init -y
Enter fullscreen mode Exit fullscreen mode

and we see our package.json file opens up πŸ‘
Next up let us install our dependencies.
These are the dependencies that we are going to use to develop this awesome system and I'll shed light πŸ’‘ on them one after another.
Then we create our index.js, env and gitignore file.

npm install cors dotenv express mongoose paystack-api
npm install --save-dev nodemon @ngrok/ngrok
touch index.js .env .gitignore
Enter fullscreen mode Exit fullscreen mode

As stated, I will shed light on each of the dependencies.

cors: CORS is a node.js package for providing a Connect/Express middleware that can be used to enable cross-origin-resource-sharing with various options for access to our API.
dotenv: Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env. Storing configuration in the environment separate from code is based on The Twelve-Factor App methodology and we are going to use it to store our API Secret Keys.
express: This is the Nodejs framework we will use to build our API.
mongoose: Mongoose is the tool that makes it easier for us to work directly and interact with our MongoDb.
paystack-api: The Pariola paystack api wrapper which does all the paystack under the hood connections in our nodejs app and provides functions for our integration.
nodemon: nodemon is a tool that helps develop Node.js based applications by automatically restarting the node application when file changes in the directory are detected.
ngrok: ngrok delivers instant ingress to your apps in
any cloud, private network, or devices
with authentication, load balancing, and other critical controls. We will be using it for our webhook event testing to connect our localhost to a live URL πŸ”₯
You can create an account here Ngrok

Nodemon and Ngrok were installed as dev dependencies because we wouldn't be needing them live unless you want to use some of ngrok's additional features.

Sooo our folder structure should look similar to this and now we startup our express server in our index.js file.

PaystackFolderStructure

index.js

// imports
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");

// specify the port from our enviroment variable
const PORT = process.env.PORT || 8080;
app.use(cors());
app.use(express.json());
// connect database

// routes

app.get('/', async (req, res) => {
    res.send("Hello World!");
});

app.listen(PORT, () => {
    console.log(
        `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`
    );
});
Enter fullscreen mode Exit fullscreen mode

.env

PORT=8000
NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode

.gitignore

node_modules
.env
Enter fullscreen mode Exit fullscreen mode

package.json

{
  "name": "paystack-nodejs-integration",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "mongoose": "^7.5.0",
    "paystack-api": "^2.0.6"
  },
  "devDependencies": {
    "@ngrok/ngrok": "^0.6.0",
    "nodemon": "^3.0.1"
  }
}

Enter fullscreen mode Exit fullscreen mode

Notice the scripts in the json file. We will be using

npm run dev

on the terminal for our development mode.

ConnectedNode
Then we try the url on postman

PostmanImage

Who dey check! (exclaims in joy)

Now let us connect to our MongoDb Compass.
MongoDbCompass

Then we set our MONGODB_URI in our .env file

MONGODB_URI=mongodb://0.0.0.0:27017/paystack-node-integration
Enter fullscreen mode Exit fullscreen mode

After that, we add our MongoDb configuration in the config/db.js file and then we save

const mongoose = require("mongoose");
mongoose.set("strictQuery", false);

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      useUnifiedTopology: true,
      useNewUrlParser: true,
    });

    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;
Enter fullscreen mode Exit fullscreen mode

Then we import and call the function in our index.js

// imports
const connectDB = require("./config/db.js");

// connect database
connectDB();
Enter fullscreen mode Exit fullscreen mode

Boom! Our database is connected

ConnectedMongoDb

Now let us create a system where we have users and they can donate for a campaign using their cards and can also be able to subscribe to a plan you created.

Let us start by creating the user model for our database.
We create a folder called models and in the folder, we create a file called userModel.js.

models/userModel.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
    fullname: {
        type: String,
    },
    email: {
        type: mongoose.Schema.Types.Mixed,
    },
    paystack_ref: {
        type: String,
    },
    amountDonated: {
        type: Number,
    },
    isSubscribed: {
        type: Boolean,
    },
    planName: {
        type: String,
    },
    timeSubscribed: {
        type: Date,
    },
});

const User = mongoose.model("user", userSchema);

module.exports = User;
Enter fullscreen mode Exit fullscreen mode

We are going to be using the common MVC (Models, Views, Controller) architectural pattern except ours will be routes endpoints instead of views.
So let's create two(2) extra folders controllers and routes to make our app easy to debug, more scalable and to be understood by other developers unless, you go explain tire (you will over explain).

We want to create two endpoints for now. The first will be to create a user and the second will be to get the user.
In our controllers we create a new file called userController.js

controllers/userController.js

const User = require("../models/userModel");
// Require paystack library

const createUser = async (req, res) => {
    let { email, fullname } = req.body;

    const user = new User({
        fullname,
        email,
    });
    await user.save();

    res.status(201).send({
        data: user,
        message: "User created successfully",
        status: 0,
    });
}

const getUser = async (req, res) => {
    try {
        let { id } = req.params;
        const user = await User.findById(id);

        res.status(200).send({
            user,
            message: "Found user Details",
            status: 0,
        });
    } catch (err) {
        res.status(500).send({ data: {}, error: err.message, status: 1 });
    }
};

// initialize transaction

module.exports = {
    createUser,
    getUser,
};
Enter fullscreen mode Exit fullscreen mode

After that, we create another folder named routes and create a file called userRoutes.js which will be used to call our controller functions.
routes/userRoutes.js

const express = require("express");
const userRoute = express.Router();

const {
    getUser,
    createUser,
} = require("../controllers/userController");

userRoute.get("/getUser/:id", getUser);
userRoute.post("/createUser", createUser);

module.exports = {
    userRoute,
};
Enter fullscreen mode Exit fullscreen mode

In order to be able to use the routes, we have to import it in our index.js file.

// routes
const { userRoute } = require("./routes/userRoutes.js");
app.use("/users", userRoute);
Enter fullscreen mode Exit fullscreen mode

So far so good! Our folder structure should look like this.
structure
Now we test our user creation endpoint on postman and see that it works and it reflects in our database 🎈
usercreationtest

db
Congratulations on making it this farπŸ‘πŸ‘ I guess we both deserve a drink 🍻
This is the moment we all have been waiting for.
Moving on to the next phase, we remember that we want to create a system where we have users and they can donate for a campaign and can also be able to subscribe to a plan you created.

These are two key implementations

  1. Campaign donations
  2. Plan Subscriptions

Campaign Donations
Imagine if we are organizing an event and we need people to monetize by donating some funds, let us create an API endpoint for this.

Before that let's go to paystacks dashboard settings and copy our secret key in order for us to be able to interact with our account. https://dashboard.paystack.com/#/settings/developers
And for this article purposes I will be on test mode so no real funds are going to be transacted.
PaystackSecretKey
⚠️Remember, on no account should you share your secret key to anyone that is why it is going to be stored in an environmental variable which will be omitted on any deployment. In case of any public display which was done mistakenly you should generate a new key. ⚠️

Adding a new line to our .env file where we copy and paste our secret key there.
.env

TEST_SECRET=YOUR_SECRET_KEY
Enter fullscreen mode Exit fullscreen mode

In our controllers/userController.js we add the following lines to initialize a transaction and we save the transaction reference to get the details about a particular transaction which you must have come across in your regular banking transactions.
https://paystack.com/docs/api/charge/#create

controllers/userController.js

// Require paystack library
const paystack = require("paystack-api")(process.env.TEST_SECRET);

// initialize transaction
const initializeTrans = async (req, res) => {
    try {
        let { id } = req.params;
        const { email, amount, plan, } = req.body;

        const response = await paystack.transaction.initialize({
            email,
            amount,
            plan, // optional but we'll use for subscription
        });

        const data = {
            paystack_ref: response.data.reference,
        };

        await User.findByIdAndUpdate(id, data);

        res.status(200).send({
            data: response.data,
            message: response.message,
            status: response.status,
        });

    } catch (error) {
        res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
    }
};

// verify transaction

module.exports = {
    ...,
    initializeTrans,
};
Enter fullscreen mode Exit fullscreen mode

If you look at the code above you notice we have 3 inputs taken into the body parameters.
email, amount and plan(which is optional but since we are also going to have subscriptions we will be using it later.

Let's add it to our userRoute
routes/userRoutes.js

const {
    ...,
    initializeTrans,
} = require("../controllers/userController");

userRoute.post("/initiatetransaction/:id", initializeTrans);
Enter fullscreen mode Exit fullscreen mode

Yes! time to test on postman.
I set the amount to be 300000 which is 3000 due to currency two decimal places and the email to be mine as payer.

postman
It works πŸ‘
We are going to click on the authorization_url and see what happens.

Paystack
Bravo friends πŸ’ͺ we can see our payment options provided and proceed to payment.
After payment, we want to verify that the payment was successful and we can achieve that in two methods.

  1. Using the paystack verify endpoint https://paystack.com/docs/payments/verify-payments/
  2. Using webhooks to listen for events https://paystack.com/docs/payments/webhooks/

In this article we will be covering this two methods πŸ₯

Let's start by verifying the transaction we just made with the verify endpoint. On verification, we add the amount the user donated and change the transaction reference to success in our database.

controllers/userController.js

// verify transaction
const verifyTrans = async (req, res) => {
    try {
        let { id } = req.params;

        const user = await User.findById(id);

        if (user.paystack_ref == "success")
            return res.status(401).send({
                data: {},
                message: "Transaction has been verified",
                status: 1,
            });

        const response = await paystack.transaction.verify({
            reference: user.paystack_ref
        });

        if (response.data.status == "success") {
            const data = {
                paystack_ref: response.data.status,
                amountDonated: response.data.amount,
            };
            await User.findByIdAndUpdate(id, data);

            return res
                .status(200)
                .send({
                    data: response.data,
                    message: response.message,
                    status: response.status,
                });
        } else {
            return res
                .status(200)
                .send({
                    data: response.data,
                    message: response.message,
                    status: response.status,
                });
        }

    } catch (error) {
        res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
    }
};

module.exports = {
    ...,
    verifyTrans,
};
Enter fullscreen mode Exit fullscreen mode

routes/userRoutes.js

const {
    ...,
    verifyTrans,
} = require("../controllers/userController");

userRoute.post("/verifytransaction/:id", verifyTrans);
Enter fullscreen mode Exit fullscreen mode

After testing the endpoint on postman, we can verify it works from our response and in our database 🀩🀩

postman

mongodb
Yay! Now we are sure this works, we can go ahead to work on the Plan Subscriptions and over here, we will get to see how we can use webhooks for event listening πŸ‘‚πŸͺ

Plan Subscriptions
In a scenario whereby you are the CEO of a kitchen app and you want people to be able to subscribe to give them access to various chefs on the app you have to create several plans for them which can either be weekly or monthly and so on. Let's create these plans and enable subscription on our app.
We can also create plans directly from our paystack dashboard but I will be doing so programatically.

We create a new file in our controllers and call it planController.js where we are going to create our createPlan, getPlan and webhook function.

controllers/planController.js

// Require the library
const paystack = require("paystack-api")(process.env.TEST_SECRET);

const createPlan = async (req, res) => {
    try {
        const { interval, name, amount } = req.body;

        const response = await paystack.plan.create({
            name,
            amount,
            interval,
        });

        res.status(200).send({
            data: response.data,
            message: response.message,
            status: response.status,
        });

    } catch (error) {
        res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
    }
};

const getPlans = async (req, res) => {
    try {
        const response = await paystack.plan.list();

        res.status(200).send({
            data: response.data,
            message: response.message,
            status: response.status,
        });

    } catch (error) {
        res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
    }
};

// our webhook function for event listening

module.exports = {
    createPlan,
    getPlans,
};
Enter fullscreen mode Exit fullscreen mode

We create a new file in our routes call planRoutes.js

routes/planRoutes.js

const express = require("express");
const planRoute = express.Router();

const {
    createPlan,
    getPlans,
} = require("../controllers/planController");

planRoute.get("/getPlans", getPlans);
planRoute.post("/createPlan", createPlan);

module.exports = {
    planRoute,
};
Enter fullscreen mode Exit fullscreen mode

Let's also not forget to import it in our index.js

index.js

// routes
const { planRoute } = require("./routes/planRoutes.js");

app.use("/plans", planRoute);
Enter fullscreen mode Exit fullscreen mode

Saving our file and testing our createPlan endpoint, we see that it works πŸ‘

createplan
We have successfully created a plan for our users to subscribe to 🍻
What next...? Yes you are right, we have to subscribe to the plan but before we do that, we are going to add our webhook functions to listen to transaction events sent by paystack.

So we create a new folder called helpers and there we create a file called webhookHelpers.js
In this file we have 3 functions that will be triggered when an event is updated chargeSuccess, planChargeSuccess and cancelSubscription. Depending on your choice of usage you can create more.
https://paystack.com/docs/payments/subscriptions/

helpers/webhookHelpers.js

const User = require("../models/userModel");

// Require the library
const paystack = require("paystack-api")(process.env.TEST_SECRET);

// Paystack webhook helpers: Functions that should be called on paystack event updates
// invoicePaymentFailed, invoiceCreation, invoiceUpdate, subscriptionNotRenewed, subscriptionDisabled, chargeSuccess

const chargeSuccess = async (data) => {
    try {
        const output = data.data;
        const reference = output.reference;
        // console.log(output);

        const user = await User.findOne({ paystack_ref: reference });
        const userId = user._id;
        console.log("Updating charge status");

        if (user.paystack_ref == "success")
            return ({
                data: {},
                message: "Transaction has been verified",
                status: 1,
            });

        const response = await paystack.transaction.verify({
            reference: user.paystack_ref
        })

        if (response.data.status == "success") {
            const data = {
                paystack_ref: response.data.status,
                amountDonated: output.amount,
            }
            await User.findByIdAndUpdate(userId, data);

            console.log("Charge Successful");
        } else {
            console.log("Charge Unsuccessful");
        }

    } catch (error) {
        console.log({ data: {}, error: `${error.message}`, status: 1 });
    }
};

// succesful subscription
const planChargeSuccess = async (data) => {
    try {
        const output = data.data;
        const reference = output.reference;
        // console.log(output);

        const user = await User.findOne({ paystack_ref: reference });
        const userId = user._id;
        // console.log(user, reference);

        console.log("Updating charge status");

        // subscribe for user
        if (user.paystack_ref == "success")
            return ({
                data: {},
                message: "Transaction has been verified",
                status: 1,
            });

        const response = await paystack.transaction.verify({
            reference: user.paystack_ref
        })

        if (response.data.status == "success") {
            await User.findByIdAndUpdate(userId, {
                isSubscribed: true,
                paystack_ref: response.data.status,
                planName: output.plan.name,
                timeSubscribed: response.data.paid_at,
            });
            console.log("Charge Successful");
        } else {
            console.log("Charge Unsuccessful");
        }

    } catch (error) {
        console.log({ data: {}, error: `${error.message}`, status: 1 });
    }
};

// invoicePaymentFailed
const cancelSubscription = async (data) => {
    try {
        const output = data.data;
        const reference = output.reference;
        // console.log(output);

        const user = await User.findOne({ paystack_ref: reference });
        const userId = user._id;

        console.log("Cancelling subscription...");

        await User.findByIdAndUpdate(userId, {
            isSubscribed: true,
            paystack_ref: response.data.status,
            planName: "cancelled",
        });
        console.log("User Subscription Cancelled");

    } catch (error) {
        console.log({ data: {}, error: `${error.message}`, status: 1 });
    }
};

module.exports = {
    planChargeSuccess,
    chargeSuccess,
    cancelSubscription,
};
Enter fullscreen mode Exit fullscreen mode

In our planController we import the helper functions
Once an action like a successful charge is created it triggers an event which we are going to select an endpoint URL for paystack to send the data to.

controllers/planController.js

// Require the library
const { planChargeSuccess, chargeSuccess, cancelSubscription, } = require("../helpers/webhookHelpers");

// our webhook function for event listening
// you can edit this to your style
const addWebhook = async (req, res) => {
    try {
        let data = req.body;
        console.log('Webhook data: ', data);

        switch (data) {
            case data.event = "invoice.payment_failed":
                await cancelSubscription(data);
                console.log("Invoice Failed");
                break;
            case data.event = "invoice.create":
                console.log("invoice created");
                break;
            case data.event = "invoice.update":
                data.data.status == "success" ?
                    await planChargeSuccess(data) :
                    console.log("Update Failed");
                break;
            case data.event = "subscription.not_renew":
                console.log("unrenewed");
                break;
            case data.event = "subscription.disable":
                console.log("disabled");
                break;
            case data.event = "transfer.success":
                console.log("transfer successful");
                break;
            case data.event = "transfer.failed":
                console.log("transfer failed");
                break;
            case data.event = "transfer.reversed":
                console.log("transfer reversed");
                break;
            case data.event = "subscription.disable":
                console.log("disabled");
                break;

            default:
                // successful charge
                const obj = data.data.plan;
                console.log("Implementing charges logic...");
                // object comparison verifying if its a normal payment or a plan
                // charges for subscription and card
                Object.keys(obj).length === 0 && obj.constructor === Object ?
                    await chargeSuccess(data) :
                    // charge sub
                    await planChargeSuccess(data);
                console.log("Successful");
                break;
        }

    } catch (error) {
        res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
    }
};
module.exports = {
    ...,
    addWebhook,
};
Enter fullscreen mode Exit fullscreen mode

We are going to call it here in the planRoutes

const {
    ...,
    addWebhook,
} = require("../controllers/planController");

planRoute.post("/paystackWebhook", addWebhook);
Enter fullscreen mode Exit fullscreen mode

To us who made it this far we deserve pounded yam, don't we? 😁
One final push!
Before we begin trying out our endpoint we need to use our ngrok tool to deploy our localhost live so we can add the url to paystack.
https://dashboard.paystack.com/#/settings/developers
webhookurl
Then we login to https://dashboard.ngrok.com/get-started/your-authtoken for our token, copy it and add it to our .env file

.env

NGROK_AUTHTOKEN=YOUR_AUTH_TOKEN
Enter fullscreen mode Exit fullscreen mode

We add the following lines of code to our index.js

index.js

// imports
const ngrok = require("@ngrok/ngrok");

// at the bottom
if (process.env.NODE_ENV == "development") {
    (async function () {
        const url = await ngrok.connect({ addr: PORT, authtoken_from_env: true, authtoken: process.env.NGROK_AUTHTOKEN });
        console.log(`Ingress established at: ${url}`);
    })();
} 
Enter fullscreen mode Exit fullscreen mode

Then we save and our terminal should look similar to this and if we click on the link we should see our Hello World!
ngrokterminal
Let's add the link to our test webhook URL on paystack so our events can be triggered. Your link should be different.
https://ce41-102-88-63-21.ngrok-free.app/plans/paystackWebhook

page
Note: Each time nodemon restarts our server, the webhook URL changes.

Great! so we test with the initiatetransaction endpoint on postman but notice this time we added a plan to the body and that amount will be overridden by the plan amount we created.
subscribe
paystackpay

webhook log

mongodbsub

Yay πŸ₯³πŸŽ‰ It works smoothly 🀩
Now we can stand up and stretch our body... Feels good πŸ˜‹

Summary
We have learnt and sharpened up lot from working with paystack transactions, REST APIs, modelling our database, building with a solid architecture, working with tools and libraries like nodemon, dotenv, mongoose, ngrok etc. and we have seen an importance of webhooks and event in developing real world applications and even cases of initializing payment process for a contribution or a subscription plan.

Paystack also has a lot more features you can explore in their documentation.

If you encounter any challenge, feel free to comment here or connect with me on LinkedIn or Twitter

Congratulations once again πŸŽ‰
Thanks for coming all the way and I wish you success in your endeavours.

Link to Github Repo
Paystack Docs

Top comments (11)

Collapse
 
omzi profile image
Omezibe Obioha

Nice write-up bro πŸ‘πŸ½.

Your article popped up on my Google Feed. The cover image (Yuji) piqued my interest πŸ˜„.

Collapse
 
kizito007 profile image
Kizito Nwaka

Thank you so much.
(Myy Brother) πŸ˜‚πŸ˜‚

Collapse
 
omzi profile image
Omezibe Obioha

Haha, nice one πŸ˜„. You're welcome (my brother).

Collapse
 
starlingroot profile image
Chidera Anichebe

Clear and concise, thanks for cooking! 🀞

Collapse
 
kizito007 profile image
Kizito Nwaka

Thanks bro πŸ™Œ
Still making more recipes 😎

Collapse
 
sammychinedu2ky profile image
sammychinedu2ky

Great write up man

Collapse
 
kizito007 profile image
Kizito Nwaka

Thanks bro πŸ’ͺ

Collapse
 
oliviaoputa profile image
Oputa Olivia Amarachi

This article was sleek.

Nice Read

Collapse
 
kizito007 profile image
Kizito Nwaka

Thanks.
That's how we roll 😎

Collapse
 
itzz_okure profile image
Okure U. Edet KingsleyπŸ§‘β€πŸ’»πŸš€

This article was really useful

Collapse
 
kizito007 profile image
Kizito Nwaka

Glad you found it useful πŸ™