Creating a RESTful Transaction API With Mongoose in NodeJS

mongoose.png A step-by-step tutorial on how to create a RESTful API With Mongoose, specifically a transaction API.

Setting Up

First, we'll need to import mongoose and get it connected to our local or cloud database. Then, we want to export the active connection. Let's call this file 'mongoose.js'.

/* Import mongoose */
const mongoose = require('mongoose');

/* Connect to database */
const mongoURI = "MONGODB_URI_HERE";
mongoose.connect(mongoURI,
    { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true})
    .then(() => console.log('MongoDB Connected...'))
    .catch(err => console.log(err));

/* Export the active connection */
module.exports = { mongoose }

Creating our Mongoose Models

Next, its time to create our models. For the transaction API, three objects will be involved: the user purchasing the item, an item that is being purchased, and last but not least, the transaction record itself.

Product Model

First, we'll create a Mongoose schema which will basically define how the object will look like, which will then be exported as a Mongoose model. Let's call this file 'product.js'

const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
    price: {
        type: Number,
        required: true
    },
    description: {
        type: String,
        required: true
    },
    name: {
        type: String,
        required: true
    },
    category: {
        type: String,
        required: true
    }
});

const Product = mongoose.model('Product', productSchema);
module.exports = { Product };

Essentially, the schema that we have created restricts a Product to have a price, description, name, and category. Alternatively, if we don't need a product to have a category property, then we could do the following instead:

const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
    ...
    category: {
        type: String,
        default: "none"
    }
    ...
});
const Product = mongoose.model('Product', productSchema);
module.exports = { Product };

User Model

Moving on to the User model, I'll call this user.js and I have designed it as follows. Feel free to change it as you need.

const { Product } = require('./product');
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    name: {
        type: String,
        maxlength: 50,
        required: true
    },
    email: {
        type: String,
        trim: true,    /* trims white spaces if any */
        unique: 1,
        required: true
    },
    history: {
        type: [Product.schema],
        default: []
    }
});

const User = mongoose.model('User', userSchema);
module.exports = { User };

Notice how the history property is dependent on the Product schema that we have created.

Transaction Model

And finally, let's call this transaction.js

const mongoose = require('mongoose');

const transactionSchema = mongoose.Schema({
    email: {
        type: String,
        required: true
    },
    data: {
        type: mongoose.Schema.Types.Mixed,
        required: true
    },
    product: {
        type: Array,
        default: []
    }
}, { timestamps: true });

const Transaction = mongoose.model('Transaction', transactionSchema);
module.exports = { Transaction };

Notice how I passed in an extra argument { timestamps: true }, which will basically generate a timestamp when the object gets created and/or updated through two properties called "createdAt" and "updatedAt" respectively.

Building the API Routes

Now comes the exciting part. Creating the actual API! First, let's export our models to a new JavaScript file, which I will call 'server.js'. For good practice, also import ObjectID from mongo, which will be used to validating object IDs. We'll also use a middleware called body parser which is basically used for parsing HTTP JSON bodies into usable object.

const express = require('express');
const app = express();

const { mongoose } = require('./db/mongoose');

// Good practice: to validate object IDs
const { ObjectID } = require('mongodb');

// Mongoose Models
const { User } = require('./models/user');
const { Product } = require('./models/product');
const { Transaction } = require('./models/transaction');

const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

POST Routes

For creating new objects using the three Mongoose models we just created. The POST route for user and products are similar and straightforward, but for our transaction POST route, we are updating the user's purchase history at the same time.

User

app.post('/user', (req, res) => {
    /* Example Request Body
    *  {
    *      name: "mongoose",
    *      email: "mongoose@mongodb.com",
    *  }
    */
    cons
    const user = new User(req.body);
    user.save().then((result) => {
        res.send(result)
    }, (error) => {
        res.status(400).send(error) // 400 for bad request
    })
});

Product

app.post('/product', (req, res) => {
    /* Example Request Body
    *  {
    *      price: 10,
    *      name: "Introduction to APIs",
    *      description: "A quick guide to learn APIs in 5 minutes",
    *      category: "Technology"
    *  }
    */
    const product = new Product(req.body);
    product.save().then((result) => {
        res.send(result)
    }, (error) => {
        res.status(400).send(error) // 400 for bad request
    })
});

Transaction

app.post('/checkout', (req, res) => {
    const { product, paymentData, user_id } = req.body;
    const transactionData = { email: paymentData.email, data: paymentData, product: product };

    // Update user purchase history
    User.findByIdAndUpdate(
        user_id,
        {$push: {history: [{paymentId: paymentData.paymentID, dateOfPurchase: Date.now(), item: product}] },
        {new: true},
        err => {
            if (err) return res.json({success: false, err});
        }
    );

    const payment = new Transaction(transactionData);
    payment.save().then((result) => {
        res.send(result)
    }, (error) => {
        res.status(400).send(error) // 400 for bad request
    })
});

GET Routes

For getting information on existing objects. For GET routes, sending a JSON request body is not possible so to send over arguments, we can use either URL query parameters (i.e., after ? in the URL) or route parameters, which we can use req.query or req.params to access respectively, instead of req.body

User

/* find a user by email using URL query as parameters*/
app.get('/user', (req, res) => {
    const email = req.query.email;
    User.findOne({ email:email }).then(user => {
        if (!user) {
            res.status(404).send()
        } else {
            res.send(user)
        }
    }).catch((error) => {
        res.status(500).send()  // server error
    })   
});

Product

/* find a product by its object id if provided else we get all products */
app.get('/product', (req, res) => {
    if (req.query.id == null){
        Product.find().then((items) => {
            res.send(items)
        }, (error) => {
            res.status(500).send(error) // server error
        })
    } else {
        if (!ObjectID.isValid(req.query.id)) { // check if its a valid object id
        res.status(404).send()
        return;
    }
        const query = {_id: req.query.id};
        Product.findOne(query).then(item => {
            if (!item) {
                res.status(404).send()
            } else {
                res.send(item)
            }
        }).catch((error) => {
            res.status(500).send()  // server error
        })
    }
});

Transaction

Essentially, it can be designed the same as either the user or product route.

DELETE Routes

For deleting existing users or objects. Personally, I think transactions don't need a DELETE route.

User

/* remove a user by email */
app.delete('/user', (req, res) => {
    const query = { _id: req.query.email };
    User.deleteOne(query,
        (err, doc) => {
            if (err) return res.json({success: false, err});
            return res.json({success: true, doc});
        }
    )
});

Product

/* remove a product by its object id using route parameters */
app.delete('/product/:id', (req, res) => {
    const id = req.params.id;
    if (!ObjectID.isValid(id)) {
        res.status(404).send();
        return;
    }
    Product.findByIdAndRemove(id).then((product) => {
        if (!product) {
            res.status(404).send()
        } else {   
            res.send(product)
        }
    }).catch((error) => {
        res.status(500).send() // server error, could not delete.
    })
});

PATCH/PUT Routes

For updating information on existing users or objects.

User

app.patch('/user', (req, res) => { 
    const { email, newEmail } = req.body;
    User.findOneAndUpdate({ email: email }, {$set: { email: newEmail }}, {new: true})
    .then((user) => {
        if (!user) {
            res.status(404).send()
        } else {   
            res.send(user)
        }
    }).catch((error) => {
        res.status(400).send() // bad request
    })
});

Product

app.put('/product', (req, res) => {
    const id = req.body._id;
    if (!ObjectID.isValid(id)) {
        res.status(404).send();
        return;
    }
    Product.findOneAndUpdate({_id: id}, req.body, {new: true},
        (err, doc) => {
            if (err) return res.json({success: false, err});
            return res.json({success: true, doc});
        }
    )
});

And that's all! Hopefully this helps anyone trying to create their own APIs using Mongoose in NodeJS.

No Comments Yet