DEV Community

Cover image for Building a Full-stack Invoicing Application using the Novu API
James Oyanna
James Oyanna

Posted on • Edited on

Building a Full-stack Invoicing Application using the Novu API

Invoicing is an essential aspect of running a business, and having an efficient invoicing system can greatly streamline operations.

With the advancements in technology, developers now have access to powerful APIs that simplify the process of creating and sending invoices.

One such API is the Novu API, which empowers developers to build robust and feature-rich invoicing applications.

This article explores the power of the Novu API and how it can be leveraged to create a full-stack invoicing application.

You will be building a fullstack invoicing application for creating and Sending invoices to customers via email. This application will be useful for Startups, Business Owners and Freelancers.

Application Features

Invoice Creation:

The application provides a user-friendly interface for creating invoices.

Users can input relevant details such as the customer's name, billing information, item descriptions, quantities, prices, and notes.

Invoice Sending:

You will integrate the Novu API, that would enable you to send invoices in PDF format directly to customers via email.

Edit Invoice Functionality:

To cater to for changing requirements or corrections, you will incorporate an edit invoice functionality.

Here is the link to the repository of the application with the full code implementation -Repo

Here is a link to the demo of the application - Novu Invoice Application

Here is a screenshot of the final look of the application:

Frontend Page View :

Front page

Invoice Creation View:

Invoice Creation

Created Invoice View:

Created Invoice

Sample Invoice:

Sample Invoice

Overview of the Novu API:

The Novu API is a unified delivery API that would allow you to send invoices as email messages to customers seamlessly.

You can read more about it on the Novu Website. You can also read-up on the documentation Here

It provides a straightforward integration process, making it an excellent choice for developers looking to incorporate invoicing functionality into their applications.

The API handles the complexities of email delivery, ensuring that invoices reach customers' inboxes reliably.

*The technologies you will be using for the development of this application are; *

  • React JS -Library for building your user interface
  • Axios - Promise based HTTP client for the browser and node.js
  • React-router - Enables the implementation of dynamic routing in a web applications.
  • Tailwind CSS - A utility-first CSS framework for your styling.
  • Novu/node - Simple components and APIs for managing all communication channels in one place: Email, SMS, Direct, and Push

  • Express JS - flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

  • HTML-PDF - HTML to PDF converter that uses phantomjs

  • Mongoose - Provides a straight-forward, schema-based solution to model our application data.

  • Node.js- Cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser.

Let's start building.
Create a folder for your project. Name it novu-invoice or you can name it anything you like.

Now, you will start with building the API or backend of the application first.

Inside the novu-invoice folder you have just created, create another folder. Name it backend.

Open your command-line inside the backend folder and type:
npm init --y to initiate your API application.

A package.json file is automatically generated for you.

Your package.json file should look like this -
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Setting up the Server:

Now, install the necessary dependencies you will be needing for this project by running this command:
npm i npm i express mongoose cors html-pdf moment pdfkit uuid @novu/node dotenv nodemon.

This will install all the dependencies.

Inside your backend folder, create a file. Name it app.js and setup a simple express server in it by adding the code below:

Here, you imported the Express.js module, which is a web application framework for Node.js.

You created an instance of the Express application with the code - const app = express();

You add a middleware to our application. In this case, express.json(), used to parse incoming JSON data.

This allows you to easily handle JSON payloads in your application routes.

You then setup a simple route for the root URL ("/") such that when a GET request is made to the root URL, the callback function (req, res) => { ... } is executed.

And then lastly, you specify a port on which the server will listen for incoming requests.

In this case, it is set to port 5000. The app.listen() function starts the server and listens on the specified port.

Once the server is running, it logs a message to the console indicating that the app is running and on that port.

Update your package.json by adding this start script - "start": "nodemon app.js" in the script section. Your package.json file should now look like this.

To start your server, run the command - npm start

This would display this on your command line, indicating the server is working.

Server Running

Establish Database Connection:

Now, implement a database connection to your application.

In your root directory, create a file. Name it db.js and add the following code inside:

Here in this code, you are establishing a connection to a MongoDB database using Mongoose, a popular MongoDB object modeling tool for Node.js.

Firstly, you imported the Mongoose module. You then define an asynchronous function called connectToDatabase.

You establish a connection to the MongoDB database using the mongoose.connect() method. The connection string is provided as the argument, which is stored in the MONGODB_URI environment variable.

If the database connection is successful, you log a success message to the console.

You exported the connectToDatabase function, allowing other parts of our application to import and use it.

In your app.js file, import the connectToDatabase function and invoke it. Add the following code to your app.js file like this:

const connectToDatabase = require("./db");
.......
connectToDatabase();

Your app.js file should now look like this:

Now create a file. Name it .env in your root directory and add the following code:
MONGODB_URI = mongodb://localhost:27017/novu-invoice

The .env file you created is to securely manage configuration variables or environment-specific settings for your application.

It allows you to separate sensitive or environment-specific configuration from your application code in this case, you have store MongoDB connection url on it.

Please ensure you setup a MongoDB Database either locally using using the MongoDB Compass(You can do that here or on the cloud using the MongoDB Atlas.

You have also installed Nodemon, a library that helps you to automatically restart our server. You can also run npm start if you have terminate the process before.

If everything works fine, you should see this in your console indicating the database is successfully connected.

DB Connected

Implement Database Model:

Now, implement your database model.
Create a folder in your project directory. Name it model. Inside this folder, create a file. Name it invoiceModel.js and add the following code:

Here in this code, you imported the necessary modules from the Mongoose library.

const invoiceSchema = new Schema({
// Schema properties and their types
});

Here, you created a new Mongoose schema named invoiceSchema using the Schema object. The schema defines the structure and properties of the "Invoice" document stored in the MongoDB collection.

You then define the properties of the Invoice schema. Each property corresponds to a field in the "Invoice" document.

The properties can have various types such as Date, String, or Number. The items property is an array that contains objects with properties such as itemName, unitPrice, and quantity.

The createdAtproperty is a special field that automatically gets assigned the current date and time when a new document is created.

module.exports = model('Invoice', invoiceSchema);

Here, you exported the Mongoose model for the "Invoice" schema. The model function is used to create a model from the invoiceSchema.

The model is given a name 'Invoice', which corresponds to the MongoDB collection name. The exported model can then be used to perform database operations on the "Invoice" collection.

Implement Invoice Controller:

Create a folder and name it controller. Inside the controller folder, create a file. Name it invoiceController.js and add the following code:

Here, you require the Mongoose library and an invoice model from the model directory.

You define a function called createInvoice that handles the creation of an invoice.

It extracts the necessary data from the request body, generates a random 6-digit invoice number with the prefix "INV," and then creates a new invoice using the InvoiceModel.

The new invoice is then sent as a JSON response with a status code of 201 (created).

You then define another function to handle the editing of an invoice.
It extracts the invoice ID from the request parameters and the invoice data from the request body.

It then checks if the provided invoice ID is a valid MongoDB object ID. If not, it sends a 404 response with the message "No invoice with that id."

If the ID is valid, it updates the invoice using the InvoiceModel's findByIdAndUpdate method, passing the ID, the invoice data, and the option { new: true } to return the updated invoice. The updated invoice is then sent as a JSON response.

Implement Routing:

To implement your routes, create a folder. Name it routes. Inside it, create a file. Name it invoiceRoute.js and add the following code:

Here in this code, you imported the Express library and the other controller functions from controllers folder.

The controller functions handle the logic for creating and editing an invoice.

You created an instance of the Express router by calling express.Router(). The router allows you to define routes for specific HTTP methods and attach the corresponding controller functions to them.

You define the following routes using the router:

POST: /
  • This route is used to create a new invoice. It calls the createInvoice controller function.
PATCH /:id
  • This route is used to update an existing invoice based on the id parameter. It calls the editInvoice controller function.

Now make use of your routes in the app.js file.
In your app.js file import the invoice route by requiring it like this:

const invoiceRoutes = require('./routes/invoiceRoute')
And use it like this:
app.use('/api/invoices', invoiceRoutes);

Your app.js file should now look like this:

Designing the Invoice Template:

In this section you will be designing the way your invoice will look like.

Inside your project folder, create a folder. Name it document. Inside the document folder create a file. Name it index.js and add the following code:

Here in this code you are basically defining a JavaScript function called pdfTemplate that generates an HTML template for your invoice.

The pdfTemplate function takes an object as an argument containing various properties related to an invoice, such as name, phoneNumber, email, notes, items, dueDate, issuedDate, totalQuantity, totalAmount, and invoiceNumber.

It uses these properties to populate the invoice template.

The returned HTML template can then be used to generate PDF documents or display the invoice in an HTML format.

You will import the invoice template into our app.js file and use it later on.

Implementing the Novu API:

Novu provides a unified API that makes it simple to send notifications through multiple channels, including In-App, Push, Email, SMS, and Chat.

With Novu, you can create custom workflows and define conditions for each channel, ensuring that your notifications are delivered in the most effective way possible. You can read more about the Novu API in their Official Doc.

You will need to sign up for a Novu account to get started

After setting up an account, the next step involves creating a workflow within the workflow tab.

Workflow tab

Here, you added the email event below the Trigger by dragging and dropping it as the event that will happen whenever a notification is triggered.

So you are setting up one that works for sending emails, as you want to send invoice as an email.

Click on the email channel you just added and fill-in the required information and click on the update button to save your changes like this :

Workflow Email field

Go to your settings tab still in your Novu Dashboard, copy the API Key and add it to your environment variable like this :

API Key

In your .env file, add the api key you have just copied like this:

NOVU_API_KEY= your-novu-api. Replace the value with your Novu API Key.

One more thing you will add to your implementation is the TRIGGER_NAME. You will get this by clicking on the Workflow tab and locate the settings area like this:

Workflow settings

In this case, the TRIGGER_NAME which is our identifier isinvoice-notification. Copy your identifier and save it somewhere. You will need it for your implementation.

Now, implement the Novu API. But before that, require some of the dependencies you will be using. Add the following to your app.js file

const { Novu } = require('@novu/node');
const pdf = require('html-pdf');
const cors = require('cors');
const fs = require('fs');
const pdfTemplate = require('./document/index');

// Enable CORS middleware
app.use(cors());

Here in this code above, you imported several Node.js modules: @novu/node, html-pdf, cors, fs, and a custom module pdfTemplate.

You also imported or require the pdfTemplate you have previously created.

Then you added the CORS middleware to the Express application using this code:

app.use(cors());

The application enables CORS and allows cross-origin requests from different domains.

Add the logic for sending the invoice as PDF like this:

Here in this code, you define an object options with a property format set to 'A4'. These options are used when generating the PDF document using the html-pdf library. In this case, the A4 format is specified.

app.post('/send-pdf', (req, res) => { ... });

You then sets up a route handler for the POST request to the /send-pdf endpoint.

You listen for incoming requests and executes the provided callback function when the endpoint is accessed via a POST request.

const { email } = req.body;
You extracted the email property from the request body using destructuring assignment.

pdf.create(pdfTemplate(req.body), options).toFile('invoice.pdf', (err) => { ... });

You generated a PDF document using the pdfTemplate function and the options defined earlier.

You executed the pdfTemplate function with the request body as an argument. The resulting PDF document is saved to a file named 'invoice.pdf'.

const novu = new Novu(process.env.NOVU_API_KEY);

With this code above, you created a new instance of the Novu object, which was imported earlier from the @novu/nodemodule.

novu.trigger('invoice-notification', { ... });

Here, you invoke the trigger method of the novu object, passing 'invoice-notification' as the first argument and an object containing notification details as the second argument. (Remember to replace the identifier with your own TRIGGER_NAME or identifier you had earlier saved.

The details include the recipient's subscriber ID (Remember to replace the subscriber ID with the one on your Novu dashboard) and email address, and an attachment object representing the invoice in PDF format.

It reads the contents of the PDF file using fs.readFileSync and converts it to a base64-encoded string before attaching it to the payload.

What you have basically done was to handle a POST request to generate a PDF invoice, save it to a file, and trigger a notification using the Novu object.

Your app.js should finally look like this:

You are done with the backend of our Novu Invoice project. You can test your endpoints using Postman like this:

Testing the create invoice endpoint:

Create Invoice Endpoint

Developing the frontend of your Invoice Application:

In this section, you will be developing the frontend of your invoice application.

Inside your project directory where you have your backend folder,
initialize a new react application by running:
npx create-react-app frontend on your command line.

This would create a new frontendfolder for your application.

Navigate into the frontend folder from your command line and install the Tailwindcss to add styling to your application using the following command:

npm install -D tailwindcss and then run
npx tailwindcss init to initialize the Tailwindcss. This will automatically generate a tailwind.config file for the setup.

Now open the application in VSCode or in any of your favorite editor.

Remove the default content in the tailwind.config and add the following code:

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

Open the index.css file in the srcfolder of your project directory. Remove its content and add the following code:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now start your application by running this command:
npm start

This will start the application on port 3000. Go to your browser. You should see your application running on http://localhost:3000/

You should see this:

React App

Now do some cleaning on your application and also test your tailwind styling to ensure its working.

In the app.js file, remove it's content and add the following code:

export default function App() {
return (
<h1 className="text-3xl font-bold underline bg-green-600">
Novu Invoice
</h1>
)
}

After applying the tailwind styling, your application should now look like this on your browser.

Tailwind style

Now implement routing:
Firstly, you will need to install the react-router-dom library for your routing. Stop your server and run the command:

npm i react-router-dom

In the src folder, create a folder. Name it components. Inside the components folder, create a file and name it Invoice.jsx and add the following code.

Here you just create a simple component to render invoice as text.

Now, update the index.js file with the following code:

Here in this code, you imported the necessary modules from the react, react-dom, and react-router-dom libraries.

You also imported the Invoice component you had created earlier.

You created a router using createBrowserRouter from react-router-dom.

Router is configured with an array containing a single route object:

The route object specifies the path as "/" (root path) and associates it with the Invoice component meaning that when the root path is accessed, the Invoice component will be rendered.

Add Logo:

In the src folder, create a folder name it images and add any image of your choice as logo.

Creating the Invoice Header:

Now, create a file inside your component folder. Name it InvoiceHeader.jsx and add the following code:

Here in this code, you define a React functional component called InvoiceHeader.

You added your logo by importing it from the images folder.

You imported Link from "react-router-dom", a package used for routing in React applications. It will allow you create links to different routes within the application.

InvoiceHeader component renders a header section for your invoice which includes a logo, title, location information, and an invoice number label.

The logo is wrapped in a link that navigates to the root path when clicked.

Import the InvoiceHeader component into the Invoice component. Your Invoice component should now look like this:

Here in this code, you imported the InvoiceHeader from the './InvoiceHeader' file.

A container is rendered with a specified width and styling, and includes the InvoiceHeader component as its child, which renders the header section of the invoice.

Your application should now look like this on your browser:

Front page

Add Invoice FormField:

Now, create a file inside your component folder. Name it FormField.jsx and add the following code to it:

Here in this code, you define the FormField component as an arrow function that takes in a destructured object as its parameter.

This object contains the following properties;
label: Represents the label text for the form field.

name: Represents the name attribute of the input field.

type: Represents the type attribute of the input field (e.g., "text", "number", "email").

value: Represents the current value of the input field.

onChange: Represents the callback function to handle the onChange event of the input field.

placeholder: Represents the placeholder text for the input field.

error: Represents an optional error message to be displayed.

This FormField component represents a form field with a label, input field, and optional error message.

It provides props for configuring various attributes and behavior of the form field, such as label text, input type, value, onChange event handler, placeholder, and error message.

You will be defining these props in the Invoice component shortly.

Now use your FormFieldcomponent in the Invoice component. You will also be adding a table and a button. Update your Invoice.jsx file with this code:

Here in this code, you imported the FormField. You rendered a form layout inside a flex container with centered content and a margin-top.

You included the InvoiceHeader component to display the invoice header section.

The form includes fields for billing information, such as the recipient's name and the issued date. It also includes fields for contact information, such as email address, phone number, and due date.

You added a table to be used for inputting item descriptions, quantities, prices, and amounts. Add Item button is present to add new items to the invoice.

Invoice summary section displays the total quantity and total amount of the invoice.

You added "Create Invoice" button at the end of the form for creating the invoice.

Your application should now look like this on your browser:

Invoice

Now, define the different states to be used in your application.

You will be adding the invoiceData, error and isLoading to track the state in your application and to also update the state.

Update your Invoice.jsx file with the following code:

Here in this code , you imported useState hook from the "react" module and useNavigate from "react-router-dom" module.

Then you initialized state variables using the useState hook:

  • isLoading:
    A boolean state to track the loading state of the component.

  • errors:
    An object that holds error messages for form fields.

invoiceData:
An object that stores the data related to the invoice, including name, phoneNumber, email, issuedDate, dueDate, items (an array of item details), notes, total, invoiceNumber, totalQuantity, and totalAmount.

You defined a navigate function using the useNavigate hook from "react-router-dom". It allows you to navigate to different routes in our application.

You added event handlers as follows:

  • handleInputChange:
    Handles the input change for form fields and updates the invoiceData state accordingly.

  • handleItemChange:
    Handles the input change for individual item fields in the items array and updates the invoiceData state. It also calls theupdateTotals function to recalculate the total quantity and total amount.

  • handleAddItem:
    Adds a new item object to the invoiceData state's items array.

handleRemoveItem:
Removes an item object from the invoiceData state's items array.

updateTotals:
Calculates the total quantity and total amount based on the items' quantity and unit price. It updates the invoiceData state accordingly.

Now, you will refactor our code a bit. You will extract the invoice items into it's own component.

Create a file inside the component folder. Name it InvoiceItem.jsx and add the following code to it:

Here in this code, you defined the InvoiceItem functional component, which represents a single row in the invoice table for an item.

The component receives props as its parameter. The props include:

item: An object containing the details of the item (e.g., itemName, quantity, unitPrice).

index: The index of the item in the list of items.

onChange: A callback function to handle changes in the item's input fields.

onRemove: A callback function to handle removing the item from the list.

This component returns JSX markup, representing a table row (

) with several table cells ().

Each table cell contains an element for user input or display of item details.

You will use this component inside your Invoice component later.

Now before you start using our state variables and the functions, you defined in the Invoice component, integrate your backend API with the frontend.

Run the command npm i axios to install the axios library for making http calls to the server. If your React app is still running, you will need to stop the server before installing the library.

Inside the src, create a file. Name it api.jsand add the following code to it:-

Here in this code, you imported axios for making HTTP requests to our API.

Using the create method, you created an Axios instance. The method accepts an object with configuration options as its parameter.

In this case, the configuration object contains a single property, baseURL which Specifies the base URL for all the requests made using the Axios instance.

Now update your Invoice.jsx with the following code:

Here in this code, you defined several event handler functions used in the form:

navigateTo: Use this navigate function to navigate to a specified route with optional state data.

handleCreateInvoice: An asynchronous function that handles the creation of the invoice.

It performs validation on the form fields, sends a POST request to the server using the api object, handles the response, and redirects to the invoice details page.

handleInputChange: function updates the corresponding field value in the invoiceData state object when an input field's value changes.

handleItemChange: function to update the details of an individual invoice item in the invoiceData state object when the corresponding input field's value changes.

It also calls the updateTotals function to recalculate the total quantity and total amount.

handleAddItem: A function that adds a new empty item to the items array in the invoiceData state object.

handleRemoveItem: this function removes an item from the items array in the invoiceData state object.

updateTotals: function calculates the total quantity and total amount based on the items' quantities and unit prices and updates the corresponding fields in the invoiceData state object.

Upon clicking the Create Invoice button to create your invoice, you need to set a page that you will use to display the detail of the invoice you have created.

Now set it up. Inside your component folder, create a file. Name it InvoiceDetails.jsx and add the following code:

Here in this code, you imported several dependencies and defined the InvoiceDetails component that renders the invoice details form.

Inside the component, you used the useLocation hook to get the current location object from the React Router.

You initialized the initialInvoiceData variable with the invoiceData value from the location state. The ?. operator is used for optional chaining to prevent errors if location.state or location.state.invoiceDatais undefined.

You defined the navigateTo function to navigate to a specified route when called. It uses the navigate function from useNavigate.

You defined the handleGoBack function to navigate back to the home page when called. It calls the navigateTo function with the "/" route.

You defined the handleSendInvoice function as an asynchronous function. It is called when the "Send Invoice" button is clicked

It sends the invoice data to the server using the api.post method and updates the loading, error, and success states based on the response.

You also defined the handleEditInvoice function as an asynchronous function. It toggles the editMode state when called.

If editMode is true, it calculates the updated total quantity and total amount based on the items in the invoice data, sends a patch request to update the invoice data on the server using the api.patch method, and updates the success and error states accordingly.

Also ensure you update your index.js file by importing InvoiceDetails into it. You use it to handle routing to the details page.

Your index.js file should now look like this:

YOU have just completed your fullstack application. You can test its functionalities by creating and invoice, add your email and other information and send the invoice to your email.

A Message will display showing an Email has been sent to the email you supplied for the receiving of the Invoice.

Here is the link to the repository of the application with the full code implementation -Repo

Here is a link to the demo of the application - Novu Invoice Application

Conclusion: This tutorial provides a details explanation on how you can integrate the Novu API on an Invoice application for sending of invoice via email.


Top comments (1)

Collapse
 
lalami profile image
Salah Eddine Lalami

Thanks for sharing, @ IDURAR , we use are using node.js react.js & redux

Here Tutorial about : 🚀 Building and Generate Invoice PDF with React.js , Redux and Node.js : dev.to/idurar/building-an-invoice-...

 Building and Generate Invoice PDF with React.js , Redux and Node.js