Serverless architectures are becoming increasingly popular, but, when using a serverless architecture to build a backend API you quickly run into a bump in the road. Does a stateless function mean you have to create a new connection to your database every time the function runs? Thankfully, most serverless architectures have a workaround for this, so you don't incur the additional latency of connecting to the database every time. There are some good articles about reusing database connections with AWS Lambda using the MongoDB driver and mongoose, but I haven't been able to find any information on Azure Functions. This article will walk you through setting up an Azure Function in Node.js that connects to MongoDB and reuses the database connection between requests.

Creating a Simple Azure Function

Log in to the Azure Portal and click "Create a resource."

Click "Compute" and then "Function App" to create a new Function App. A Function App is a collection of individual functions.

Azure will ask you for information about your function app. For the purposes of this article, all you need to do is give your app a name. Just beware that your app name needs to be unique across all apps, not just yours. In this example, I prefixed the app name with my GitHub username.

Click "Create" and Azure will kick off creating your Function App. Click the bell icon on the upper right to look out for a notification that Azure finished creating your app.

Once Azure has created your app, click "Go to resource" to configure your Function App.

Next, create a new function.

Choose the "Webhook + API" scenario and "JavaScript" as your language, and then click "Create this function."

Azure will give you a code editor to create your function. Use the below JavaScript for your function. The 'Content-Type' header is necessary because Azure Functions respond with XML by default.

module.exports = function (context, req) {
  context.res = {
    headers: { 'Content-Type': 'application/json' },
    body: { res: 'Hello, World' }
  };
  context.done();
};

Click on "Get Function URL" to get the publicly accessible endpoint for your function.

You should now be able to curl your function with this URL.

$ curl https://vkarpov15-test.azurewebsites.net/api/HttpTriggerJS1?code=OMITTED
{"res":"Hello, World"}
$

Installing the MongoDB Driver on Azure

Now that you've set up your first function, let's make the function connect to a database. Adding npm modules to your Azure Function is very different from the bundling process that AWS Lambda requires. With Azure Functions, you have to log in to a server, create a package.json, and run npm install. This seems odd for a "serverless" architecture, but the benefit is you don't have to bundle the same dependencies over and over again or worry about node_modules running over Lambda's onerous limits on function size.

To install the MongoDB Node.js driver, first go to <your-function-name>.scm.azurewebsites.net, and click on 'Debug Console' -> 'PowerShell'.

This will open up a live shell you can use to administer your function app.

Navigate to the D:\home\site\wwwroot directory and click on the plus icon to create a new file called package.json.

Create the below minimal package.json file and hit 'Save'.

{
    "name": "vkarpov15-test",
    "dependencies": {
        "mongodb": "3.x"
    }
}

Next, run npm install from the shell.

Now, go back to your Azure Function and you should be able to connect to MongoDB Atlas and execute a query using the below code.

const mongodb = require('mongodb');

// URI for MongoDB Atlas
const uri = 'mongodb+srv://OMITTED.mongodb.net/test';

module.exports = function (context, req) {
  context.log('Running');
  mongodb.MongoClient.connect(uri, function(error, client) {
    if (error) {
      context.log('Failed to connect');
      context.res = { status: 500, body: res.stack }
      return context.done();
    }
    context.log('Connected');

    client.db('test').collection('tests').find().toArray(function(error, docs) {
      if (error) {
        context.log('Error running query');
        context.res = { status: 500, body: res.stack }
        return context.done();
      }

      context.log('Success!');
      context.res = {
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ res: docs })
      };
      context.done();     
    });
  });
};

Reusing the Database Connection

Unfortunately, creating a new database connection every time the function runs is terrible for performance. Like with Lambda, you can use the quirks of the Node.js runtime to retain a database connection between calls. Specifically, global variables in your script may be retained between function calls, so if you add a global pointer to a MongoDB client it will be retained until Azure cleans it up.

const mongodb = require('mongodb');

const uri = 'mongodb+srv://OMITTED/test';

// May be retained between function executions depending on whether Azure
// cleans up memory
let client = null;

module.exports = function (context, req) {
  context.log('Running');

  let hasClient = client != null;

  if (client == null) {
    mongodb.MongoClient.connect(uri, function(error, _client) {
      if (error) {
        context.log('Failed to connect');
        context.res = { status: 500, body: res.stack }
        return context.done();
      }
      client = _client;
      context.log('Connected');
      query();
    });
  } else {
    query();
  }

  function query() {
    client.db('test').collection('tests').find().toArray(function(error, docs) {
      if (error) {
        context.log('Error running query');
        context.res = { status: 500, body: res.stack }
        return context.done();
      }

      context.log('Success!');
      context.res = {
        headers: { 'Content-Type': 'application/json' },
        body: 'Num docs ' + docs.length + ', reused connection ' + hasClient
      };
      context.done();     
    });
  }
};

The first time you hit the API endpoint, the function will create a new database connection. The second time, the function will re-use an existing database connection and run considerably faster.

$ time curl https://vkarpov15-test.azurewebsites.net/api/HttpTriggerJS1?code=OMITTED
"Num docs 1, reused connection false"
real    0m3.070s
user    0m0.097s
sys    0m0.017s
$
$
$
$
$ time curl https://vkarpov15-test.azurewebsites.net/api/HttpTriggerJS1?code=OMITTED
"Num docs 1, reused connection true"
real    0m0.981s
user    0m0.104s
sys    0m0.021s
$

Moving On

Azure Functions are an interesting alternative to AWS Lambda. Azure Functions don't require you to bundle node_modules, instead letting you share node_modules across multiple functions in a function app. Also, Azure charges you based on memory usage rather than setting a hard limit on memory usage like Lambda does. I have yet to use Azure Functions for a production application, but so far Azure Functions look promising and I look forward to trying them out for a real app.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus