Writing a MongoDB Web Service Wrapper
Introduction
MongoDB is a popular open-source document database that is commonly used for building web and mobile applications. While MongoDB provides native drivers and a robust API for directly interacting with a MongoDB database from code, there are some benefits to abstracting the database interaction behind a web service layer. This article will walk through how to build a simple yet flexible MongoDB web service wrapper using Node.js, Express, and the MongoDB Node.js driver. By building a web service wrapper, we can gain advantages like abstracting implementation details from clients, adding authorization/authentication, caching, aggregation capabilities, and more.
Design Considerations
Before jumping into the code, let’s discuss some important design considerations for the MongoDB web service wrapper:
API Design – We’ll want to design a RESTful JSON API that is intuitive for clients to use yet flexible enough to support various use cases. Consider the basic CRUD operations like create, read, update, delete as well as more advanced functionality.
Authentication/Authorization – Clients will need to authenticate and be authorized to access different resources. We can implement basic auth, JSON web tokens, or an OAuth flow depending on needs.
Input Validation – Strongly validate all incoming request data to prevent malicious operations and client errors.
Error Handling – Have a consistent way of returning errors to the client with helpful error codes and messages.
Async Processing – Database operations are async, so the web service wrapper API should support async functions and return promises rather than blocking.
Request Caching – Caching read operations in memory can boost performance when the same data is frequently requested.
Connection Pooling – MongoDB connections should be pooled to avoid establishing new connections for each request.
Aggregation Support – Higher level aggregation operations should also be supported through the API.
Logging/Monitoring – Service operation and errors should be logged for debugging and monitoring.
Documentation – API documentation is essential for clients to easily understand and work with the service.
Testing – Code testing helps prevent regressions and ensures high quality.
With these considerations in mind, let’s look at building out a basic Node.js/Express web service wrapper that meets many of these requirements.
Project Setup
We’ll use the Node.js project setup tool called npm to initialize a new project and install dependencies. Open your terminal and run:
Copy
npm init -y
npm install express mongoose
This will create a package.json file and install Express for building the web service and Mongoose as the MongoDB ODM (Object Data Mapping) driver.
Next, create an index.js file that will act as the entry point:
js
Copy
const express = require(‘express’);
const app = express();
app.get(‘/’, (req, res) => {
res.send(‘Hello World!’);
});
app.listen(3000, () => {
console.log(‘Server listening on port 3000’);
});
We instantiate an Express app and add a basic route handler. Now run node index.js to start the server on port 3000. If you see the “Hello World!” message, the service is up and running!
Connecting to MongoDB
Next we’ll connect to MongoDB using Mongoose. First install the mongoose package:
Copy
npm install mongoose
Then open the mongoose.js file to configure the connection:
js
Copy
const mongoose = require(‘mongoose’);
// MongoDB URI
const uri = ‘mongodb://localhost:27017/mydatabase’;
mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
db.on(‘error’, console.error.bind(console, ‘connection error:’));
db.once(‘open’, () => {
console.log(‘MongoDB connected!’);
});
module.exports = mongoose;
This connects to a local MongoDB instance at the default port using the Mongoose connect method. The connection callbacks handle errors and log a message on successful connection.
Now import and require the mongoose configuration file from index.js to establish the database connection on service start.
Creating Models
Mongoose models define the schema and attributes of data documents that will live in MongoDB collections. For example, if we want to store users in a “users” collection, we’d define a User model:
js
Copy
// user.model.js
const mongoose = require(‘./mongoose’);
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
module.exports = mongoose.model(‘User’, userSchema);
Now we have a Mongoose model named ‘User’ that represents user documents with name, email and password attributes.
Additional models for other collections like posts, comments, etc. would be defined in their own files similarly. These models will then be used throughout the app and API implementation.
Building the REST API
With Mongoose models defined, we’ll build out RESTful routes to perform CRUD operations on the MongoDB collections via the API.
We’ll start with a POST route to create a new user document:
js
Copy
// routes/users.js
const router = require(‘express’).Router();
const User = require(‘../models/user.model’);
router.post(‘/’, async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json({message: ‘User created’});
} catch (err) {
res.status(500).json({error: err});
}
});
module.exports = router;
This route validates and saves the new user data to the database using the User model. Errors are caught and handled cleanly.
Additional CRUD routes would be implemented similarly:
js
Copy
// GET all users
router.get(‘/’, async (req, res) => {…});
// GET single user
router.get(‘/:id’, async (req, res) => {…});
// UPDATE user
router.put(‘/:id’, async (req, res) => {…});
// DELETE user
router.delete(‘/:id’, async (req, res) => {…});
We can also build out routes for additional resources like posts, comments, etc. Each route would validate inputs, perform the MongoDB operation via Mongoose models, catch errors and return consistent JSON responses.
These router files are then required and mounted in the index.js file:
js
Copy
const userRouter = require(‘./routes/users’);
app.use(‘/users’, userRouter);
Now the REST API routes are hooked up and ready to use!
Testing and Usage
Manual testing of the API can be done using tools like Postman to send sample requests. But automated testing is important too. We can write unit and integration test cases using a framework like Jest.
For example, a test to create a new user:
js
Copy
// user.test.js
const request = require(‘supertest’);
const app = require(‘../index’);
describe(‘POST /users’, () => {
it(‘responds with 201 and creates user’, async () => {
const res = await request(app)
.post(‘/users’)
.send({
name: ‘Test’,
email: ‘test@example.com’,
password: ‘password’
});
expect(res.status).toBe(201);
expect(res.body).toHaveProperty(‘message’);
});
});
These tests ensure the API works as expected and protect against regressions as code changes.
Documentation should also be generated – either programmatically via annotations or manually via a site like Swagger. This allows clients to seamlessly consume and leverage the functionality provided through the API.
Some additional enhancements may include authentication/authorization middleware, request caching, connection pooling, logging, analytics, and aggregation functionality on data access. But this covers the core components to build a simple yet production-ready MongoDB web service wrapper.
The goals of abstracting the database layer, input validation, error handling, async support and consistent interface are met through this approach. Clients can now easily consume database functionality without worrying about low level driver details.
