How to Develop a Web App With Netlify Serverless Functions and MongoDB
Rate this tutorial
As I mentioned in a previous tutorial, I'm a big fan of Netlify and the services they offer developers—my favorite of their services being their static website hosting and serverless functions. When combining the two, you can deploy a complex website or web application with a back end, without ever worrying about infrastructure or potentially complex API design.
So how does that work with a database like MongoDB?
If you've ever dabbled with serverless functions before, you might be aware that they are not always available for consumption. Many serverless functions exist for a defined amount of time and then are shut down until they are executed again. With this in mind, the connection to a database might not always be available like it would be in an application built with Express Framework, for example. You could establish a connection every time a function is called, but then you risk too many connections, which might overload your database.
So what do you do?
In this tutorial, we're going to see how to build and deploy a simple web application using Netlify Functions and MongoDB Atlas. For this particular tutorial, we'll be using the Node.js driver for MongoDB, but both Netlify and MongoDB support the use of other languages and drivers as well.
There are a few things that must be ready to go prior to starting this tutorial.
- Node.js 15+ must be installed.
- You must have a Netlify account.
Everything we do in this tutorial can be done for free. You can use the free forever tier of MongoDB Atlas, which is an M0 sized cluster. You can also use the free plan that Netlify offers.
It's worth mentioning that if you prefer a completely serverless experience, MongoDB Atlas also offers serverless instances—a fully serverless database that scales elastically with demand. We won't be exploring serverless instances in this tutorial, but if you'd like to learn more, check out this article.
If you need help getting started with your MongoDB Atlas cluster, check out this previous tutorial I wrote on the subject.
To keep this tutorial simple and easy to understand, we're going to work from a new project. While you don't necessarily need to use the CLI to get the job done, it will be easier for this example.
Assuming the Netlify CLI is installed, execute the following command to create a new project:
1 netlify sites:create
For this example, we won't be using the continuous integration features that you'd get when connecting GitHub and similar. Make sure to follow the instructions that the CLI presents you with from the above command.
We won't be creating any particular project files until we progress through the tutorial.
The star of this tutorial is going to be around properly including MongoDB Atlas in your Netlify Function. As previously mentioned, you have to take into consideration the state of the function to prevent attempting to access a database without a connection or establishing too many connections.
With that in mind, we can do the following with the Netlify CLI:
1 netlify functions:create --name get_movies
Follow the instructions when prompted. For this example, we'll be using JavaScript rather than TypeScript or Golang. While not too important because we'll be making significant changes after, select hello_world as the function template that you want to use.
Since we'll be using the MongoDB Node.js driver, we need to install it into our project. From the command line, execute the following at the root of the project:
1 npm install mongodb
Netlify serverless functions will be able to access NPM modules at the root of the project.
If you kept the default function path, open the netlify/functions/get_movies.js file and add the following JavaScript code:
1 const { MongoClient } = require("mongodb"); 2 3 const mongoClient = new MongoClient(process.env.MONGODB_URI); 4 5 const clientPromise = mongoClient.connect(); 6 7 const handler = async (event) => { 8 try { 9 const database = (await clientPromise).db(process.env.MONGODB_DATABASE); 10 const collection = database.collection(process.env.MONGODB_COLLECTION); 11 // Function logic here ... 12 } catch (error) { 13 return { statusCode: 500, body: error.toString() } 14 } 15 } 16 17 module.exports = { handler }
So what is happening in the above code?
The first thing we're doing is creating a new MongoDB client using a URI string that we're storing within our environment variables. While you could hard code this URI, your best bet is to keep it in an environment variable because we'll be using them in a later step of this tutorial.
Probably the most interesting line here is the following:
1 const clientPromise = mongoClient.connect();
If you've ever worked with the MongoDB Node.js driver before, you've probably used this with an
await
or similar. We don't want to block our function in the global area. This area before the handler
function will be executed only when the function starts up. We know that the function won't necessarily shut down after one execution. With this knowledge, we can use the same client for as long as this particular function exists, reducing how many connections exist.Inside the
handler
function, we can attempt to resolve our client promise, get a handle to the database we want to use, and get a handle to the collection we want to use. For this example, we are storing the database name and collection name in an environment variable.Let's expand upon our function to actually do something useful. Change the JavaScript code to look like the following:
1 const { MongoClient } = require("mongodb"); 2 3 const mongoClient = new MongoClient(process.env.MONGODB_URI); 4 5 const clientPromise = mongoClient.connect(); 6 7 const handler = async (event) => { 8 try { 9 const database = (await clientPromise).db(process.env.MONGODB_DATABASE); 10 const collection = database.collection(process.env.MONGODB_COLLECTION); 11 const results = await collection.find({}).limit(10).toArray(); 12 return { 13 statusCode: 200, 14 body: JSON.stringify(results), 15 } 16 } catch (error) { 17 return { statusCode: 500, body: error.toString() } 18 } 19 } 20 21 module.exports = { handler }
For this particular function, we are using our connection to find all documents in our collection, limiting the result set to 10 documents, and returning the data to whatever executed the function. Our example is technically around movies documents, but our code doesn't really reflect anything that specific.
So we've got a serverless function for querying our MongoDB Atlas database. Let's tie it together with some of the other Netlify offerings, particularly the website hosting.
At the root of our Netlify project, create an index.html file with the following markup:
1 2 <html> 3 <head></head> 4 <body> 5 <h1>MongoDB with Netlify Functions</h1> 6 <ul id="movies"></ul> 7 <script> 8 (async () => { 9 let results = await fetch("/.netlify/functions/get_movies").then(response => response.json()); 10 results.forEach(result => { 11 const listItem = document.createElement("li"); 12 listItem.innerText = result.title; 13 document.getElementById("movies").appendChild(listItem); 14 }); 15 })(); 16 </script> 17 </body> 18 </html>
We don't need to worry about doing anything fancy from a UI perspective in this example.
In the above HTML and JavaScript, we are setting a placeholder unordered list element. In the JavaScript, we are fetching from our serverless function, looping through the results, and adding each of our items as list items to the unordered list. Since we're assuming that we're using movie data, we are only adding the title of the movie object to the list.
Again, nothing fancy, but it proves that our function works.
We have a function, we have a website, and we're pretty much ready to deploy. Before we do that, let's make sure everything works locally.
Using the Netlify CLI, execute the following from the root of your project:
1 netlify dev
The above command should serve the function and site locally, and then open a browser for you. If you run into problems, you might check the following:
- Did you remember to set your environment variables first?
- Is your local IP address added to your MongoDB Atlas cluster?
To be honest, those two items got me, so I thought it was worth noting them.
Assuming you got everything working in your local environment, we can focus on the deployment to Netlify.
The first thing we'll want to do is create our environment variables. This can be done within the Netlify online dashboard or directly with the Netlify CLI. For this example, we'll use the CLI.
From the command line, execute the following:
1 netlify env:set MONGODB_URI YOUR_URI_HERE 2 netlify env:set MONGODB_DATABASE sample_mflix 3 netlify env:set MONGODB_COLLECTION movies
The above command sets three environment variables. You'll want to swap the URI with that of your own cluster. You can get this information within the MongoDB Atlas dashboard.
The other two variables, which represent the database and collection, are using the sample dataset. You can change them to whatever you want.
With the variables in place, we can deploy the project. From the command line, execute the following command:
1 netlify deploy
It might take a bit of time for the build process to kick in remotely, but when it's done, you should have a very basic website that consumes information from a Netlify Function.
It's worth noting that you still need to add proper network access rules to MongoDB Atlas to allow connections from a Netlify Function.
You just saw how to use Netlify Functions with the MongoDB Node.js driver to create a website with a serverless back end.
Remember, when working with serverless functions, you have to be aware that your functions may not always be available to reuse a connection. You also have to be aware that connecting to a database directly within a function handler might establish too many connections. When done properly, you'll have a powerful serverless function that can take full advantage of MongoDB.
If you're interested in another serverless function example, check out my tutorial titled Add a Comments Section to an Eleventy Website with MongoDB and Netlify for more ideas.