Recently I have been working with the team here at Footasylum on building a serverless API using Azure Functions.
The API needed to be simple and lightweight, we were already using Azure and had familiarity with Functions (we write a lot of them) as well as using Cosmos DB as a dynamic store of data.
We wanted to store primary product information from the ERP and make it accessible to other parts of the business via a web API, thus reducing the load on the EPR and creating a way to the other parts of the business to embellish the primary information, so that it’s fit for customer use.
As we didn’t want to impose too many restrictions on what we received, Javascript felt like the right choice of language for the function.
The system needed a way to create, read and update any records and can be seen in the diagram below:
Azure API Management is a great tool for managing access and control over the consumers of our APIs and we use it as the entry point for all the microservices we are developing, more on this another time.
Functions and their Magic
With the system overview out of the way lets discuss the implementation details of the functions. I’ll focus on the Create Product Function first.
To begin we initialised a function app via the command line tooling func init
and then created the function within that using func new
. The selected runtime of the app was Node and the type of function here was a HTTP trigger.
When the commands complete you are left with some initial boilerplate app structure like below:
where the boilerplate code looks something like this:
module.exports = async function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
if (req.query.name || (req.body && req.body.name)) {
context.res = {
// status: 200, /* Defaults to 200 */
body: "Hello " + (req.query.name || req.body.name)
};
}
else {
context.res = {
status: 400,
body: "Please pass a name on the query string or in the request body"
};
}
};
The func new function creates an index.js file, which is the entry point into the function as well as a function.json file, the contents of which looks like this:
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
For the CreateProduct function we needed to take the request body and save it into the cosmos DB.
Traditionally as developers we would want to create or use some sort of client library to mediate and control the persistence of data objects into some sort of database, however the “magic” here is in the function.json file.
In my (somewhat limited) understanding of the concept, the bindings represent actions that the function can complete in the given context. When the func new
command runs, it creates a binding that’s related to the type of function we are creating, but the rules aren’t limited to just one binding or type, as well will see.
When we selected the type “httpTrigger” this created some boilerplate configuration in the function.json (see picture above) we can see a type “httpTrigger” as well as a direction of “in” and a name of “req”, along with httpTrigger specific parts like “methods”. Below that we can see another binding of type “http”, name “res” and direction “out”.
The bindings here determine the input and the output of this function, and bind various bits to objects that are matched to the name property.
For example in the code above, we can see a req parameter that gets passed into the function. This represents the httpTrigger request object that carries all sort of information related to the http request it has received, such as the query string or the body of the request.
On the reverse we can see the context.res creates the response, that is sent back out of the function to the caller.
The behaviour here is determined by those binding properties.
In the case of the Create Product Function we simply needed to define a binding to handle saving data to the cosmos:
{
"name": "newProduct",
"type": "cosmosDB",
"leaseCollectionName": "leases",
"connectionStringSetting": "Products_DOCUMENTDB",
"databaseName": "Products",
"createIfNotExists": "true",
"collectionName": "Products",
"createLeaseCollectionIfNotExists": "true",
"direction": "out"
}
As shown above, we can see that we define a type of “cosmosDB” and define its direction as “out”, along with a connection string (this is actually the key in key value pair stored in the functions configuration), a database name, a collection name and whether or not to create it if it doesn’t exist.
Ready for the fun part? Saving the incoming payload now distills down to a single line of code. Below is the full function code, with some validation.
module.exports = async function (context, req) {
if (!req || !req.body ) {
context.res = {
status: 400,
body: {
Success: false,
Message: "Received an empty payload"
}
};
}
else {
context.bindings.newProduct = req.body;
context.res = {
status: 200,
body: {
Success: true
}
};
}
};
Hopefully you spotted the line of code for saving the payload to the cosmos but in case you didn’t it’s this line here:
context.bindings.newProduct = req.body;
As we can see the context object that has been passed in contains the bindings object, with our cosmosDB binding which we named newProduct and that’s all that is required to save the payload to the cosmosDB.
Reading Cosmos Data
Now that we were saving stuff, we needed a way to retrieve it. First step was to create a new ReadProduct function, again this would be a http trigger type, configured to respond to a GET with some route parameters.
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"methods": [
"get"
],
"route":"Product/{productId}"
},
{
"name": "res",
"type": "http",
"direction": "out"
}
In the above we have added in some extra binding details in the form of the route attribute, this simply appends the definition on the funcitons URL. The ProductId here is the internally generated ProductId coming in from the ERP system.
Hopefully you’ve guessed the next step, adding in a cosmosDB binding with a direction of “in”
{
"type": "cosmosDB",
"name": "readProduct",
"databaseName": "Products",
"collectionName": "Products",
"connectionStringSetting": "Products_DOCUMENTDB",
"direction": "in",
"sqlQuery": "SELECT * from c where c.ProductId = {productId}"
}
The only other noteworthy part in the above binding is the sqlQuery, here we defined a SELECT to get us all the data in a document of a given productId.
Function code
module.exports = async function (context, req, readProduct) {
if (!readProduct || readProduct.length === 0) {
context.res = {
status: 404,
body: {
Message: "Product not found"
}
};
}
else {
context.res = {
status: 200,
body: readProduct
};
}
};
In the above, we have defined a parameter readProduct, this is the same as the name of the cosmosDB binding we defined in the function.json.
When this function is called the productId we pass in the URL route parameter is taken by the function and injected into the SQL query defined on the cosmos binding, if a record is found then it is assigned to readProduct and subsequently returned on the context.res (the http out binding in the function.json)
Cool, now we are saving and reading records out of the cosmos DB, but what about updating records?
Update Product Function
Ready for the coolest part in all of this?
So an update would require, an incoming payload and the original record and that would be and in and an out on the cosmos DB. Again we created another HTTP function, similar to the read, however we also then combined the Create stuff to create the following function.json
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"put"
],
"route":"Product/{productId}"
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"type": "cosmosDB",
"name": "readProduct",
"databaseName": "Products",
"collectionName": "Products",
"connectionStringSetting": "Products_DOCUMENTDB",
"direction": "in",
"sqlQuery": "SELECT * from c where c.ProductId = {productId}"
},
{
"name": "updateProduct",
"type": "cosmosDB",
"leaseCollectionName": "leases",
"connectionStringSetting": "Products_DOCUMENTDB",
"databaseName": "Products",
"collectionName": "Products",
"direction": "out"
}
Here you can see that we have 2 entries for the cosmos DB a readProduct and an updateProduct which are in and out respectively.
The code for the function can be see below:
module.exports = async function (context, req, readProduct) {
context.log('JavaScript HTTP trigger function processed a request.');
if (!req || !req.body) {
context.res = {
status: 400,
body: {
Success: false,
Message: "Received an empty payload"
}
};
}
else if (!readProduct || readProduct.length === 0) {
context.res = {
status: 404,
body: {
Message: "Product not found"
}
};
}
else {
var cosmosId = readProduct[0].id;
var updateProduct = req.body
updateProduct.id = cosmosId;
context.bindings.updateProduct = updateProduct;
context.res = {
status: 200,
body: {
Success: true
}
};
}
};
It works in the same way as the previous two functions, the productId is passed in via a route parameter into the SQL of the cosmosDB “in” binding and any object that is found is assigned to the read product, in the else clause we then create the updateProduct object, assign the value of the request body and then append the cosmosId from the readProduct result.
When the line
`context.bindings.updateProduct = updateProduct;`
is called this overwrites the existing document with the new document passed in on the body of the update request.
And that is it. Thanks for sticking with me on this, hopefully you can see how powerful (and easy) it is to get a web API up and running with Node functions and cosmos DB.
Top comments (0)