Introducing mongo-http.js

An HTTP-based MongoDB Atlas connector for serverless runtimes (e.g., Cloudflare Workers and Deno Deploy) using the native Fetch API

Patrick Chiu
Better Programming

--

image by author | logos by respective companies

Use Cases

  • mongo-http.js can be used when the client doesn’t support raw TCP connection but supports HTTP connection, e.g., certain serverless runtimes or Cloudflare Workers
  • It can also be used in serverless runtimes where the reuse of a MongoDB connection may not always be available or require manual caching
  • Sadly, it cannot be used on the browser side yet, due to CORS. Here is a thread to request the CORS feature

Motivation

A few months ago, I migrated Medium Rare (a Chinese articles scraper and searcher on Medium) from Heroku to Cloudflare Workers. I used the MongoDB Data API to replace the MongoDB Node.js driver. During the journey, I felt that I repeatedly made fetch calls to the Data API, and I do miss the familiar API of the Node.js driver. Therefore, I built this thin wrapper on top of the Data API, which I (and hopefully others) can use in future serverless projects!

Setup MongoDB Atlas to get the app ID and API key

Before using mongo-http.js (MongoDB Data API), you need to get the app ID and API key. See more detail in the MongoDB Atlas tutorial.

Get the App ID here
And get the API key here

Installation

npm install mongo-http --save

Initialization

You can choose to initialize a client, a database or a collection as shown below. Usually, we would want to initialize a database connection.

You can initialize a client to connect to multiple databases as well.

import { initClient } from 'mongo-http';
const client = initClient({
appId: process.env.appId,
apiKey: process.env.apiKey,
});
const db = client.database({ databaseName: process.env.databaseName });
const result = await db.collection('articles').find({
filter: {
$or: [{ categories: { $in: ['javascript', 'reactjs', 'nodejs', 'mongodb'] } }],
},
});

How to initialize a database

import { initDatabase } from 'mongo-http';
const db = initDatabase({
appId: process.env.appId || '',
apiKey: process.env.apiKey || '',
databaseName: process.env.databaseName || '',
});
const result = await db.collection('articles').find({});

How to initialize a collection

import { initCollection } from 'mongo-http';
const articlesCollection = initCollection({
appId: process.env.appId || '',
apiKey: process.env.apiKey || '',
databaseName: process.env.databaseName || '',
collectionName: 'articles',
});
const result = await articlesCollection.find({});

This mirrors how the MongoDB Node.js driver initializes these three instances.

const { MongoClient } = require("mongodb");
const client = new MongoClient(process.env.uri);
await client.connect();

const db = client.db("<your database>");

const collection = db.collection("<your collection>");
const document = await collection.findOne({});

Tutorial and Examples

You can find the GitHub README.md API doc here. The input parameters mirror the Data API but adopt similar APIs of the Node.js driver.

In the following, let’s dive into how to create (insertOne), read (find), update (updateOne) and delete (deleteOne) a document. Suppose we have two collections — articles and writers and the document looks like this:

const article = {
_id: 'article-1-object-id',
writerId: 'patrick-writer-object-id',
title: 'Migrating a Node.js App to Cloudflare Workers From Heroku',
tags: ['javascript', 'cloudflare-workers', 'heroku', 'nodejs']
}

const writer = {
_id: 'patrick-writer-object-id',
name: 'Patrick Chiu',
latestArticlesAt: '2023-01-01T12:15:00.000Z'
}

1. insertOne

Let’s say we have scraped an article and would like to insert the document to articles.

const result = await db.collection('articles').insertOne({
writerId: 'patrick-writer-object-id',
title: 'Introducing mongo-http.js',
tags: ['javascript', 'mongodb', 'serverless', 'nodejs', 'cloudflare-workers']
});

// If the insertion is successful
// => isSuccess: true, insertedId: 'object-id'
const { isSuccess, insertedId } = result;

2. find

Then, we develop an endpoint /articles that supports the users to search by tags. Here, I use hono.js as the web framework.

import { Hono } from 'hono';
const app = new Hono();

// For illustration purpose,
// I skip a lot of details, such as error handling
app.get('/v1/articles', async (c) => {
const { tags = '' } = c.req.query();

if (tags === '') {
return c.json({ articles: [] });
}

const tagArray = tags
.split(',')
.slice(0, 10)
.map((tag) => tag.trim());

const result = db.collection('articles').find({
filter: { tags: { $in: tagArray } }
});

const { isSuccess, documents } = result;
return c.json({ articles: documents || [] });
});

3. updateOne

Apart from scrapping articles, we also want to update the latestArticlesAt of a user.

const result = db.collection('writers').updateOne({
filter: { writerId: 'patrick-writer-object-id' },
update: {
latestArticlesAt: '2023-01-04T19:30:00.000Z'
},
});

// If the update is successful
// => isSuccess: true, matchedCount: 1, modifiedCount: 1
// Since it is an existing document, upsertedId will be nil
const { isSuccess, matchedCount, modifiedCount, upsertedId } = result;

4. deleteOne

If the user removes their Medium account, we want to delete the document from user.

const result = await db.collection('writers').deleteOne({
filter: { writerId: 'non-existing-writer-object-id' },
});

// If the delete is successful
// => isSuccess: true, deletedCount: 1
const { isSuccess, deletedCount } = result;

Conclusion

In future articles, I hope to demonstrate full examples of using mongo-http with different web frameworks (e.g., hono.js and itty) in different serverless runtimes (e.g., Cloudflare Workers, Digital Ocean functions, and Deno Deploy)

Want to Connect?

If you find the package and tutorial useful,
please give mongo-http a look and connect with me on LinkedIn!

--

--

https://patrick-kw-chiu.github.io/ Full stack dev from HK🇭🇰 | Cache Cloud | mongo-http.js | Create tools that may or may not be useful