DEV Community

Will Velida
Will Velida

Posted on • Originally published at towardsdatascience.com on

Creating a Serverless API with Azure Functions and MongoDB

Learn how you can use Azure Functions to create a Serverless API that uses MongoDB Altas as a back-end.

In this tutorial, we’re going to build a Serverless API using HTTP Triggers in Azure Functions that uses a MongoDB Atlas backend. We’re going to build an API for a hypothetical music library that stores information about Albums.

We’re going to build this API using C#, so you’d need to at least understand the C# syntax to follow along. I’m not going to go dive into huge detail about Azure Functions and the complexities behind MongoDB, I’ll just be keeping it simple enough to demonstrate how they can all work together to make a simplistic API.

If you want to learn more about MongoDB, I’d highly recommend that you check out MongoDB University. They provide fantastic free courses for both developers and DBA’s who want to learn all about MongoDB. If you like this article and want to learn more about Azure Functions, I suggest you check out the docs.

What is MongoDB?

MongoDB is a NoSQL document database that provides developers with flexibility when it comes to indexing and querying. Data is stored in JSON documents, which allows us to change data structures over time.

MongoDB Atlas is essentially the managed service for MongoDB that we can use for our MongoDB clusters. We can host our MongoDB Atlas clusters on Azure, AWS or GCP.

For C# development, we can use the MongoDB Driver to connect to our MongoDB cluster. It’s important when using it during development to use a compatible driver with the Mongo version that your Atlas cluster is using.

What are Azure Functions?

Azure Functions are small pieces of code that we can run in Azure without worrying too much about application infrastructure (this ‘worry’ varies from scenario to scenario. It really depends on the situation and your requirements).

We can use specific events to trigger actions in our Functions. For this tutorial, we’ll be triggering events based off HTTP requests.

Set up the MongoDB Atlas Cluster

Before we start coding, we need to set up our MongoDB Atlas cluster. The MongoDB documentation has a fantastic guide on how to set up your Atlas cluster, so if you haven’t got one already, check out this guide.

Create the Azure Function

Once you’ve created your cluster, let’s create our Azure Function. I’m going to use Visual Studio 2019 to develop my function. In order to develop Azure Functions using Visual Studio, make sure that you have the Azure Development workload enabled.

Create a “New Project” and click on “Azure Functions”. Like I said earlier, we’re going to be doing this project in C# so make sure that’s the language selected in the templates.

Now that you’ve selected the right template, give your API a name and set an easy to find location to save the project it.

Now, we can select what type of Function that we wish to create. Since this is an API, I’m going to create a Function with HTTP Triggers. When you create HTTP Trigger Functions, you can set what Storage account that the Function will use along with the Authorization level.

We’re not going to be doing anything with Authorization here, so just set it to “Anonymous”. A quick comment about authorization, if you create a Function with a certain level but decide you need to change it later, you can do this in the code, so don’t fret about this setting too much. It’s just to help Visual Studio generate a template for you.

Click Create and congrats! You now have an Azure Function. It’s not great though so let’s do something about that.

Connect to MongoDB Cluster

Sweet, now that we’ve set up our function, let’s grab our connection string to our MongoDB Atlas cluster.

Head to your Atlas portal (if you’re not there already) and sign in. Click the ‘connect’ button as seen below:

Since we’re connecting an application to our cluster, select the Connect Your Application option for our connection method:

Now we have to select a driver version. Choose “C#/.NET” as the driver and “2.5 or later” as the version. Copy the connection string as we’ll need it for later.

Now, let’s create a new class called Startup.cs. This class will allow us to use Dependency Injection in our Functions.

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using MongoMusic.API.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

[assembly: WebJobsStartup(typeof(Startup))]
namespace MongoMusic.API.Helpers
{
    public class Startup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            builder.Services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.AddFilter(level => true);
            });

            var config = (IConfiguration)builder.Services.First(d => d.ServiceType == typeof(IConfiguration)).ImplementationInstance;

            builder.Services.AddSingleton((s) =>
            {
                MongoClient client = new MongoClient(config[Settings.MONGO_CONNECTION_STRING]);

                return client;
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In my Startup class, I’m instantiating a Singleton instance of my MongoClient so all functions can use it. All we need here is to get our connection setting for our MongoDB Atlas cluster and pass it through as a string parameter.

I’ve put this actual connection string in my local.settings.json file which is picked up in the IConfiguration instance of config. It’s not a good idea to hard code connection strings or settings into your code, so this is one method in your code that you can use to protect secrets needed for your application.

Setting up our Album Class

Now let’s set up our Album class. We’re not going to do anything too amazing with this class. I’ve kept it simple just to highlight a couple of things we need/can do to make this model work with the MongoDB Driver. The definition for our class is as follows:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;
using System.Text;

namespace MongoMusic.API.Models
{
    public class Album
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; set; }

        [BsonElement("Name")]
        public string AlbumName { get; set; }
        public string Artist { get; set; }
        public double Price { get; set; }
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

    }
}
Enter fullscreen mode Exit fullscreen mode

In this class, I’ve given my Album an Id, Name, Artist, Price, ReleaseDate and Genre. Real simple. The two things I want to highlight here are the Id property and the AlbumName property.

For the Id property, I’ve assigned this property to be our ObjectId. This acts as the Primary Key for our MongoDB Document. The MongoDB Driver will generate this for us.

For the AlbumName property, I’ve decorated the property as a BsonElement called Name. This is what the property will be called in our document when it’s persisted to MongoDB.

Create the RESTful methods

We’ve got pretty much everything we need to make our RESTful API, so let’s start building it. For all our functions, I’ve created a constructor that takes in the following parameters which will help inject our dependencies:

-MongoClient: To connect to our database.
-ILogger: So we can log activity in our Function. If you deploy a Function to Azure and enable Application Insights, you this where the logs get sent to.
-IConfiguration: This is what I’m using to manage all the secrets needed in our Functions. As I mentioned before, these values are kept in local.settings.json for local debugging.

I’ve also created a IMongoCollection collection so we can work with our Album collection within our MongoDB database.

For all our functions, if we get an Exception, I’ve just logged the exception message and set our IActionResult returnValue to throw a 500 error response. Nothing complex, but simple enough for this example.

Now let’s dive into each function 😊

CreateAlbum.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MongoDB.Driver;
using Microsoft.Extensions.Configuration;
using MongoMusic.API.Models;
using MongoMusic.API.Helpers;

namespace MongoMusic.API.Functions
{
    public class CreateAlbum
    {
        private readonly MongoClient _mongoClient;
        private readonly ILogger _logger;
        private readonly IConfiguration _config;

        private readonly IMongoCollection<Album> _albums;

        public CreateAlbum(
            MongoClient mongoClient,
            ILogger<CreateAlbum> logger,
            IConfiguration config)
        {
            _mongoClient = mongoClient;
            _logger = logger;
            _config = config;

            var database = _mongoClient.GetDatabase(_config[Settings.DATABASE_NAME]);
            _albums = database.GetCollection<Album>(_config[Settings.COLLECTION_NAME]);
        }

        [FunctionName(nameof(CreateAlbum))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "CreateAlbum")] HttpRequest req)
        {
            IActionResult returnValue = null;

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            var input = JsonConvert.DeserializeObject<Album>(requestBody);

            var album = new Album
            {
                AlbumName = input.AlbumName,
                Artist = input.Artist,
                Price = input.Price,
                ReleaseDate = input.ReleaseDate,
                Genre = input.Genre
            };

            try
            {
                _albums.InsertOne(album);
                returnValue = new OkObjectResult(album);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Exception thrown: {ex.Message}");
                returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }


            return returnValue;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this function, we pass through a request of type HttpRequest and read the body of that request. I’ve used Newtonsoft.Json to deserialize my request into my Album object and I’m creating a new album object with the values of our input.

I’m then using the .InsertOne() method to insert our new Album object into our MongoDB and setting my return value to a new OkObjectResult with our album object.

GetAlbum.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MongoDB.Driver;
using Microsoft.Extensions.Configuration;
using MongoMusic.API.Models;
using MongoMusic.API.Helpers;

namespace MongoMusic.API.Functions
{
    public class GetAlbum
    {
        private readonly MongoClient _mongoClient;
        private readonly ILogger _logger;
        private readonly IConfiguration _config;

        private readonly IMongoCollection<Album> _albums;

        public GetAlbum(
            MongoClient mongoClient,
            ILogger<GetAlbum> logger,
            IConfiguration config)
        {
            _mongoClient = mongoClient;
            _logger = logger;
            _config = config;

            var database = _mongoClient.GetDatabase(_config[Settings.DATABASE_NAME]);
            _albums = database.GetCollection<Album>(_config[Settings.COLLECTION_NAME]);
        }

        [FunctionName(nameof(GetAlbum))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Album/{id}")] HttpRequest req,
            string id)
        {
            IActionResult returnValue = null;

            try
            {
                var result =_albums.Find(album => album.Id == id).FirstOrDefault();

                if (result == null)
                {
                    _logger.LogWarning("That item doesn't exist!");
                    returnValue = new NotFoundResult();
                }
                else
                {
                    returnValue = new OkObjectResult(result);
                }               
            }
            catch (Exception ex)
            {
                _logger.LogError($"Couldn't find Album with id: {id}. Exception thrown: {ex.Message}");
                returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }

            return returnValue;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this function, I’m passing through an id that will represent the id of our document. Within our try/catch statement, I’ll attempt to find the album document with that id using the .Find() method on our MongoCollection and return it to the user. If we can’t find it, we’ll send a log message saying we can’t find it and throw a 404.

GetAllAlbum.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MongoDB.Driver;
using Microsoft.Extensions.Configuration;
using MongoMusic.API.Models;
using MongoMusic.API.Helpers;

namespace MongoMusic.API.Functions
{
    public class GetAllAlbums
    {
        private readonly MongoClient _mongoClient;
        private readonly ILogger _logger;
        private readonly IConfiguration _config;

        private readonly IMongoCollection<Album> _albums;

        public GetAllAlbums(
            MongoClient mongoClient,
            ILogger<GetAllAlbums> logger,
            IConfiguration config)
        {
            _mongoClient = mongoClient;
            _logger = logger;
            _config = config;

            var database = _mongoClient.GetDatabase(_config[Settings.DATABASE_NAME]);
            _albums = database.GetCollection<Album>(_config[Settings.COLLECTION_NAME]);
        }

        [FunctionName(nameof(GetAllAlbums))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Albums")] HttpRequest req)
        {
            IActionResult returnValue = null;

            try
            {
                var result = _albums.Find(album => true).ToList();

                if (result == null)
                {
                    _logger.LogInformation($"There are no albums in the collection");
                    returnValue = new NotFoundResult();
                }
                else
                {
                    returnValue = new OkObjectResult(result);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"Exception thrown: {ex.Message}");
                returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }

            return returnValue;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this function, all we’re doing here is using the Find() function to find all the albums within our Album collection and return those to the user as a list. If there are albums in the collection, we’ll return a 404.

UpdateAlbum.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MongoDB.Driver;
using Microsoft.Extensions.Configuration;
using MongoMusic.API.Models;
using MongoMusic.API.Helpers;

namespace MongoMusic.API.Functions
{
    public class UpdateAlbum
    {
        private readonly MongoClient _mongoClient;
        private readonly ILogger _logger;
        private readonly IConfiguration _config;

        private readonly IMongoCollection<Album> _albums;

        public UpdateAlbum(
            MongoClient mongoClient,
            ILogger<UpdateAlbum> logger,
            IConfiguration config)
        {
            _mongoClient = mongoClient;
            _logger = logger;
            _config = config;

            var database = _mongoClient.GetDatabase(_config[Settings.DATABASE_NAME]);
            _albums = database.GetCollection<Album>(_config[Settings.COLLECTION_NAME]);
        }

        [FunctionName(nameof(UpdateAlbum))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "Album/{id}")] HttpRequest req,
            string id)
        {
            IActionResult returnValue = null;

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            var updatedResult = JsonConvert.DeserializeObject<Album>(requestBody);

            updatedResult.Id = id;

            try
            {
                var replacedItem = _albums.ReplaceOne(album => album.Id == id, updatedResult);

                if (replacedItem == null)
                {
                    returnValue = new NotFoundResult();
                }
                else
                {
                    returnValue = new OkObjectResult(updatedResult);
                }              
            }
            catch (Exception ex)
            {
                _logger.LogError($"Could not update Album with id: {id}. Exception thrown: {ex.Message}");
                returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }

            return returnValue;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The UpdateAlbum is a little different. Here we’re passing through our request body and an id. I’m deserializing our body into an Album object and then setting the id that we pass through to our updatedResult item.

I use the ReplaceOne() method to do two things. Firstly, use the id to find the album in our collection with that id and then secondly, I pass through our updatedResult Album object to replace the existing item. If we can’t find this item, I’ll throw a 404.

DeleteAlbum.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MongoDB.Driver;
using Microsoft.Extensions.Configuration;
using MongoMusic.API.Models;
using MongoMusic.API.Helpers;

namespace MongoMusic.API.Functions
{
    public class DeleteAlbum
    {
        private readonly MongoClient _mongoClient;
        private readonly ILogger _logger;
        private readonly IConfiguration _config;

        private readonly IMongoCollection<Album> _albums;

        public DeleteAlbum(
            MongoClient mongoClient,
            ILogger<DeleteAlbum> logger,
            IConfiguration config)
        {
            _mongoClient = mongoClient;
            _logger = logger;
            _config = config;

            var database = _mongoClient.GetDatabase(_config[Settings.DATABASE_NAME]);
            _albums = database.GetCollection<Album>(_config[Settings.COLLECTION_NAME]);
        }

        [FunctionName(nameof(DeleteAlbum))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "Album/{id}")] HttpRequest req,
            string id)
        {
            IActionResult returnValue = null;

            try
            {
                var albumToDelete = _albums.DeleteOne(album => album.Id == id);

                if (albumToDelete == null)
                {
                    _logger.LogInformation($"Album with id: {id} does not exist. Delete failed");
                    returnValue = new StatusCodeResult(StatusCodes.Status404NotFound);
                }

                returnValue = new StatusCodeResult(StatusCodes.Status200OK);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Could not delete item. Exception thrown: {ex.Message}");
                returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }

            return returnValue;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In our DeleteAlbum function, again we pass through the id of the document that we wish to delete and use the DeleteOne() function to find the album that has that id and then delete it.

Testing our API using POSTMAN

All our methods are in place, so we can now start testing our API using Postman. Let’s test each one of our functions in turn. Press F5 in Visual Studio to start our Function.

It shouldn’t take too long, but once it has finished, you should be given some URL’s for each Function like so:

Keep this console window open as these URL’s are mapped to each function and we’ll need to use these to trigger our Functions.

For our CreateAlbum function, we’ll need to send the following JSON payload to insert an Album into our collection. This is the payload that I’ve sent:

{
  "albumName": "No.6 Collaborations Project",
  "artist": "Ed Sheeran",
  "price": 15.99,
  "releaseDate": "2019-12-06T11:00:00Z",
  "genre": "Pop"
}
Enter fullscreen mode Exit fullscreen mode

Paste the URL into the textbox and make sure the type is set to POST. Click “Send ”and we should get the following response:

Looks like it worked! Head back to your Atlas cluster, navigate to your Albums collections and click “Find”. We should be able to see the inserted documents with the values that we sent as part of our JSON payload.

Let’s attempt to read the item that we’ve just created! Copy and paste the _id field of the item and then use it as the id paramter in our GetAlbum URL. Change the request type to GET and click “Send”.

As you can see, we should see the document of the created album returned to us in the body.

After a quick look on Wikipedia, this album covers a few more genres so we need to update our document. Using the same id, change the request type to PUT and update the body like so:

{
  "albumName": "No.6 Collaborations Project",
  "artist": "Ed Sheeran",
  "price": 15.99,
  "releaseDate": "2019-12-06T11:00:00Z",
  "genre": "Pop, Rap, Hip-Hop"
}
Enter fullscreen mode Exit fullscreen mode

Click “Send ”to update our document:

Head to your collection in Atlas and click “Find”. We should see the updated document persisted in MongoDB:

Now let’s test our Function to see if we can delete documents. Use the Id as the id parameter and set the request type to DELETE. Click “Send ”to delete the document:

Head back to Atlas and click “Find ”again. If it worked, the document should be deleted from our Album collection:

Conclusion

In this tutorial, we learnt how we can build a really simple API using Azure Functions that uses MongoDB as a datastore. While this was a very simple project to do, hopefully, this tutorial has given you some ideas as to how you can use MongoDB in your Azure Functions.

If you want to see the whole sample, check out the code on GitHub!

If you have any questions, please let me know in the comment section below!

Top comments (3)

Collapse
 
djnitehawk profile image
Dĵ ΝιΓΞΗΛψΚ • Edited

8.73 seconds for POST
52.01 seconds for PUT

is this typical when working with azure functions?

or is low latency out the window when working with azure functions?

Collapse
 
willvelida profile image
Will Velida

Which function did you run first?

I find that when I run Functions locally, it does take a while for the initial execution, then subsequent executions are a lot quicker.

Once deployed, Functions on the Consumption plan can take a while to execute (cold starts).

Collapse
 
djnitehawk profile image
Dĵ ΝιΓΞΗΛψΚ

ah i see...
btw i was just looking at your postman timings.