StephenCookDev

Jamming with MongoDB and Netlify Netlify can support a MongoDB database — a tale in having your cake and eating it too

two people shaking hands, photoshopped to have their heads replaced by the Netlify and MongoDB logos Posted on by Stephen Cook

You enter the realm of Jamstack and approach the Netlify Council. They spot the MongoDB requirement in your app, and a long and painful silence stretches out.

Will they allow this afront to Jamstack?

Fortunately, Netlify isn’t a one-trick pony, and it does allow integration with a database, like MongoDB.

FaunaDB Warning

Quick PSA: if you’re creating a database from scratch, not moving across an existing database, then you should seriously consider using FaunaDB instead. Netlify doesn’t have first-party support for MongoDB, so what we have to do in this guide is less secure.

Also — don’t take drugs, and stay in school. Now back to our regularly scheduled programming.

Getting Started with Netlify

First things first, you’ll need to get a basic build set up on Netlify. I would recommend following this guide and coming back here once you have a site being built and deployed.

Netlify Functions

We’ll be using Netlify Functions to let our site connect to our database. These are Lambda functions that run dynamically on the server, but we’ll write alongside the rest of our code.

First, let’s add a functions field to our netlify.toml (or create a netlify.toml if you haven’t already).

[build]
  # Directory with the serverless Lambda functions
  functions = "lambda_functions"

Great! Now let’s create a Lambda function, to see what’s what. Create a new file called what_is_the_time.js in that folder:

// ./lambda_functions/what_is_the_time.js

// This `handler` is what is called when your Lambda
// function is triggered. For more full specs on it see
// https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html
module.exports.handler = async (event, context) => {
  const unixTime = Math.floor(Date.now() / 1000);
  return {
    statusCode: 200,
    body: `The Unix time is ${unixTime}`,
  };
};

Now, if we deploy this, we should be able to call this function by making a GET request to /.netlify/functions/what_is_the_time — and see what the time is:

Our website showing us the current Unix time, from the server

Huzzah!

Dependencies with Netlify Functions

As much as it’s great to know what the time is, we eventually want to be able to access our MongoDB database. To do this, we want to access a MongoDB client — so let’s download that.

npm install mongodb

We can run this just from our project root, Netlify will make sure that our Lambda functions get access to it properly. Note that in this guide I’m using mongodb@3.

Access Data from our Database

As I mentioned earlier in this guide, I assume you’ve already got a MongoDB database ready to go. If not, I highly recommend that you instead look at using FaunaDB on your Netlify site. FaunaDB has dedicated Netlify support, which MongoDB sadly does not have — meaning we have to do some less-than-perfect things to get things working with MongoDB.

So let’s create a new Lambda function to access our database. I’m going to call mine pokemon.js since my database holds a collection of my favourite Pokémon. It’s a crucial database and one that I desperately need access to through the web.

// ./lambda_functions/pokemon.js

const MongoClient = require("mongodb").MongoClient;

const MONGODB_URI = process.env.MONGODB_URI;
const DB_NAME = 'REPLACE_ME_WITH_YOUR_DB_NAME';

let cachedDb = null;

const connectToDatabase = async (uri) => {
  // we can cache the access to our database to speed things up a bit
  // (this is the only thing that is safe to cache here)
  if (cachedDb) return cachedDb;

  const client = await MongoClient.connect(uri, {
    useUnifiedTopology: true,
  });

  cachedDb = client.db(DB_NAME);

  return cachedDb;
};

const queryDatabase = async (db) => {
  const pokemon = await db.collection("pokemon").find({}).toArray();

  return {
    statusCode: 200,
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(pokemon),
  };
};

module.exports.handler = async (event, context) => {
  // otherwise the connection will never complete, since
  // we keep the DB connection alive
  context.callbackWaitsForEmptyEventLoop = false;

  const db = await connectToDatabase(MONGODB_URI);
  return queryDatabase(db);
};

Note that we reference a process.env.MONGODB_URI at the start. We can’t inline this URI in the code since it will include our DB credentials.

So first of all, let’s grab our MongoDB URI. You can follow this if you’re unsure what that is, but it should look something like this:

mongodb+srv://ashketchum:supersecurepassword123@cluster0-2cka2.mongodb.net/test?retryWrites=true&w=majority

And we’re going to put that into a Netlify build variable (which our Lambda function can access), and call it MONGODB_URI like so:

Adding MongoDB URI environment variable

Whitelisting (Here Be Dragons)

So at this point, you may notice that making a GET request to /.netlify/functions/pokemon still doesn’t work. Unfortunately, MongoDB comes by default with whitelist protection, meaning that it only allows specific IP ranges to access your database.

And unfortunately, our Lambda functions come from a variable IP range, we can’t safely say what the IP will be, so we can’t whitelist anything.

Usually, we would try and give our Lambda function a fixed IP, or create a Network Peering Connection — sadly Netlify doesn’t (at the time of writing this) support either of these things.

So instead, what Netlify recommend is that we open up our database to the entire internet.

What this means is that anyoneanywhere can access your database, if they know your password. So before doing this, please make sure that your password is secure, and that he database user has as few permissions as possible — I recommend creating a new user specifically for this.

It’s essential to get this right because you’re risking someone getting root privileges to your database and all of its data. Now would be an excellent time to get a colleague or friend to look over your shoulder!

Hopefully, I’ve done enough to impress the importance and danger of this — but making your database open to the internet will result in /.netlify/functions/pokemon returning the contents of our database!

Modify Data in our Database

So we’ve got access to our database now, but what if we want to modify data in it? How do we get data from our Lambda function?

Let’s modify our pokemon.js Lambda function to add a Pokémon when receiving a POST.

// ./lambda_functions/pokemon.js

const pushToDatabase = async (db, data) => {
  const pokemonData = {
    name: data.name,
    number: data.number,
  };

  if (pokemonData.name && pokemonData.number) {
    await db.collection("pokemon").insertMany([data]);
    return { statusCode: 201 };
  } else {
    return { statusCode: 422 };
  }
};

module.exports.handler = async (event, context) => {
  // otherwise the connection will never complete, since
  // we keep the DB connection alive
  context.callbackWaitsForEmptyEventLoop = false;

  const db = await connectToDatabase(MONGODB_URI);

  switch (event.httpMethod) {
    case "GET":
      return queryDatabase(db);
    case "POST":
      return pushToDatabase(db, JSON.parse(event.body));
    default:
      return { statusCode: 400 };
  }
};

Now we can test this by making a quick POST request to our endpoint:

// test script

const postRequest = await fetch("/.netlify/functions/pokemon", {
  method: "POST",
  body: JSON.stringify({
    name: "Pikachu",
    number: 25,
  }),
});

console.log("POST request status code", postRequest.status);

const newGetRequest = await fetch("/.netlify/functions/pokemon");
const newListJson = await newGetRequest.json();

console.log("GET request new result", newListJson);
Making a POST request to add Pikachu to our DB

Huzzah!

Summary

You should now know how to:

But you can also use this example GitHub repo for reference.