For a very long time, I used to be a C# developer. I like where the language is evolving and the confidence it brings to you while you coding. Having said that, I also like the dynamic world of TypeScript, which has a much more flexible typing system. My only problem was with the different Microsoft SDKs that are easier to use on C# and this included the NodeJS Azure functions SDK, which was… inconvenient, mildly speaking.
Don’t get me wrong, it’s changing. More and more languages are getting better SDKs from Microsoft themselves. And the new V4 programming model for NodeJS Azure functions is definitely a great example of that! The new model is much closer to what is currently used in other web frameworks and it even allows you to have multiple functions in the same file, just like in C# Azure Function! For me, personally, this is a huge game changer!
Feel free to jump around the post to the part that interests you:
- Why Azure Functions (or serverless in general)?
- The art of triggers and bindings
- Prerequisites
- NodeJS Azure Functions upvote service example
- Wrap up and final thoughts
Why Azure Functions (or serverless in general)?
Hey, you. You’re finally awake!
I wanted to make sure that you are aligned with the basic concepts of Azure Functions, just in case. If you feel comfortable with your knowledge, feel free to simply skip ahead to the demo (or even to the final code). But if you’re not sure about your knowledge, keep reading. I will try to be brief and explain only the parts that are needed to understand this post. Deal?
There are plenty of advantages of going serverless. Some are obvious right away and some are obvious only after you work with serverless technologies for a while. Let’s explore!
- Scale
Generally speaking, you want to be able to scale according to your actual load. So if you have a sudden spike in requests, your code should still be able to handle it. Luckily for us, Azure Functions are great for that and it is managed entirely for us! In most cases, you don’t even need to configure anything! - Cost
As much as we would hate to admit it, we need to take into account the cost it would take to run our code and how much it will cost us to develop it. With serverless technologies, you pay for what you use and only for that. In addition to the previous point about scaling, the mechanism works in both ways. If your function app is not being used, it will scale to 0, resulting in no cost at all. While for an individual task it might be more costly to run your code in a serverless environment, considering monthly costs, it is usually better to go serverless. - Less headache
I’m not sure about you, but I hate configurations and hate managing the environment. While it is something that absolutely needed to be done, it doesn’t mean I enjoy it. Going serverless solves most of these issues because it is being managed for us by the cloud!
So to summarize, it’s basically a win-win-win situation! Scale? ✅ Costs? ✅ Less headache? ✅✅✅
The art of triggers and bindings
Mastering the Thu’um
In the world of Azure Functions, the concepts of triggers and bindings are just like the dragon souls of Skyrim’s Thu’um. Just as the Dragonborn absorbs souls to harness shouts, developers use triggers and bindings to create responsive, dynamic applications. While the analogy sounds fun, it has a real meaning: Mastering triggers and bindings is essential for every Azure Functions warrior and will help you in every battle in the clouds.
Triggers: The Call to Action
Triggers are the events that prompt your Azure Function to execute. Think of them as the starting point of any function, a call to action that awakens your code from slumber. Here are a few common examples:
- HTTP triggers make your function respond to HTTP requests, turning it into a web endpoint.
- Timer triggers schedule your function to run at regular intervals, perfect for daily tasks or cleanup jobs.
- Queue triggers activate your function in response to messages being added to Azure Queue storage, ideal for processing data asynchronously.
Imagine setting up a timer trigger as setting an alarm clock for your function, telling it, “Hey, time to wake up and do some work!” Except, in this case, “some work” might be cleaning up database entries or sending out a batch of emails.
Bindings: The Tools of the Trade
Bindings, on the other hand, are your function’s connection to other Azure services, allowing for seamless input and output without the hassle of managing service clients or writing boilerplate code. They’re like the enchanted gear that boosts your abilities, only these enchantments help you read from a database or write to a file storage with minimal fuss.
- Input bindings let your function automatically read data from an external source, such as a Cosmos DB document or a blob in Azure Storage.
- Output bindings enable your function to easily write or output data to various services like sending a message to an Azure Service Bus or updating a table in Azure Table Storage.
For example, with an output binding to Azure Blob Storage, your function can effortlessly save files without you manually managing connections and uploads. It’s like having an enchanted quill that writes reports to your archive with a simple command.
Bringing It All Together
To see the magic in action, consider a function designed to process orders from a queue. The trigger is the arrival of a new message in the Azure Queue Storage (our call to action), and the function springs into action. It reads the order details (e.g., Storage account table input binding), processes the order, and then updates an inventory database (CosmosDB output binding) and sends a confirmation email (SendGrid output binding). This seamless flow from trigger to input binding, to custom processing code, to output bindings, demonstrates the efficiency and power of Azure Functions.
While the ancient art of the Skyrim’s Thu’um is tempting, the real magic lies in mastering Azure Functions’ triggers and bindings. They may not grand you the power of the dragonborn to let you control the weather or summon dragons, but they do empower you to build responsive, scalable cloud applications—a feat equally worthy of a legend in the realm of cloud computing.
Prerequisites
Tools of the Azureborn
Just like before a major quest in Skyrim, before you start writing your first Azure Function app, you should prepare your gear and equipment. This should be pretty straight forward, just a few installs, but if you need more details, something is not working for you, or you simply want to check it, go through the official Quickstart: Create a function.
In short, you need to install the following:
- NodeJS
Make sure you have the LTS version - Azure Functions Core Tools
Install the latest version of the tools (includes the CLI that we are going to use). This will insure that you can create and run the Azure Functions locally on your machine before publishing it to Azure. - VS Code
While I personally recommend VS Code for how ready to go it is with all the needed extensions, I understand that some of you might prefer other IDEs (e.g., WebStorm). You should be able to work with any TypeScript and NPM supporting IDE of your choice, but some things will be available for you only via the CLI.- If you choose to go with VS Code, it’s recommended (but not a must) to install the Azure Functions extension for VS Code.
- Azurite
This tool helps to emulate the storage account for some Azure Functions scenarios.
You don’t have to install it to run your first “Hello World” function, but trust me, you will need this down the road.
Depending on how you want to use it and in which environment, there are a few options.
You can use the VS Code Extension (My personal recommendation), but if this doesn’t suit your IDE preferences, there are other options. Here is the full Microsoft Learn article about it. - TypeScript
(skip this step if you hate yourself and want to make your life hell)
You probably already have it if you worked with other TypeScript projects
The above should give you enough to start with. The tools are pretty lean and light so even a lighter machine should be able to handle those.
Take your time to make sure you’ve installed everything correctly. It’s important to make sure that the installation went through successfully, otherwise you’re at risk of hair tiering and hating NodeJS Azure Functions for no real reason.
NodeJS Azure Functions upvote service example
The Civil War of Azure Functions
In the spirit of friendly rivalry, (unlike the serious fight between the Stormcloaks and the Imperial Legion in Skyrim), we’re about to embark on a quest to create an upvote service. This service isn’t about dragons or destiny; it’s about settling a debate: which is better, C# or TypeScript Azure Functions? We’ll build a service that allows our fellow developers to cast their votes.
Create a new functions app
My favorite way of creating function apps is through the CLI.
Open up a console window and navigate to the directory which will be the home of your new function app.
cd "C:/some-dir/fus-ro-vote"
func init --worker-runtime "node" --language "typescript"
PowerShellThis will create all the necessary boilerplate and even install the needed package for you, using NPM.
If you prefer to work with in your IDE instead, you can use the Azure Functions extension to do so. I’m not going to cover in details the process, but you can have a look at this Microsoft Learn article.
Create the upvote endpoint (fus)
Now, let’s create our first function! This function will be triggered by an HTTP endpoint and it will upvote one of the languages. This is our first function, so I will put extra care to explain a few concepts and ideas here.
In general, to create a function, the file in which the function is present should match the pattern in the main
property that is defined in the package.json
file. By default, that would be any file that you will save under the src/functions
directory. And since the new model supports a more flexible file system, you can even put multiple functions in the same file, which makes a lot of sense in many cases.
And while you can still use the func new
command from the CLI, with how easy it is to create a function in the new model, this command became redundant, IMHO.
Let’s start by creating our votes.ts
file and create a simple “Hello World” http trigger function with no content.
import { app } from "@azure/functions";
app.post('upvote', {
handler: () => ({ body: "Hello World" })
})
NodeJS Azure FunctionsThat’s all it takes to create an http triggered function. From here on, we will only add different options, feed data to our function, and implement our logic.
But first, let me explain what’s going on here, because although it’s just 5 lines, a lot is going on.
Line 1: The @azure/functions
package exposes the object that allows you to register new functions to the app
Line 3: By using the app.post
function, we are registering a new function to our app with a POST method http trigger.
Line 3: The first parameter is the name of the function. This should be a unique identifier of the function in the app.
Line 4: This is the function options object. For this first initial “Hello World” function, we are only creating the handler for the function.
If you have setup everything correctly, you should be able to start your function app locally by running npm start
from your console and make a simple POST request to your new endpoint.
Now, let’s add to our route a parameter that will indicate what language we are upvoting.
import { app } from "@azure/functions";
app.post('upvote', {
route: 'upvote/{language}',
handler: () => ({ body: "Hello World" })
})
NodeJS Azure FunctionsNote the {language}
parameter. This will allow us to access the language as a parameter from our code or as a setting in the bindings we are going to use.
Speaking of which, let’s add the input and output bindings of our function!
First, let’s start from getting the current score for the provided language as an input binding. And if we don’t have the score, it means that no one voted for it yet, so let’s put a 0 there.
But to make this work, you will have to do 2 things first!
- Install the Azurite VS Code extension in case you haven’t already. You will need to simulate a storage account locally for this code to run. So after making sure you installed the extension and restarted your VS Code, you should see 3 new buttons on your lower right corner.
Make sure you click the [Azurite Table Service] button to start the table emulation.
- Create the upvotes table in your emulator. The easier way to do that is to use the Azure Storage VS Code extension. After installing, simply go to the Azure tab on your left panel and select the Workspace (local) list you will see the emulator account, given you enabled the proper services from the above step.
- If you want more control over your emulated storage account, including exploring the data you have there, download the Azure Storage Explorer tool.
After you are done with the above, let’s add the code!
import { app, input } from '@azure/functions';
const upvotesInputBinding = input.table({
connection: 'AzureWebJobsStorage',
tableName: 'upvotes',
partitionKey: 'upvote',
rowKey: '{language}',
});
app.post('upvote', {
extraInputs: [upvotesInputBinding],
route: 'upvote/{language}',
handler: (request, context) => {
const upvotes = context.extraInputs.get(upvotesInputBinding);
console.log(upvotes);
return { body: 'Hello World' };
},
});
NodeJS Azure FunctionsNote the 3 added sections:
Lines 3-8: We define the binding here. Note the rowKey
prop, which gets our language
route parameter in a dynamic way.
Line 11: Adding the binding to the function itself.
Line 13: We added the request
and the context
to our handler signature.
Lines 14-15: We grab the binding value from our context
and write it to the console.
You can learn more about this specific binding in the Azure Table input binding official docs.
Now, let’s read the actual upvote value and also add an output binding so we can increase the amount of upvotes.
The output binding for table storage does not support updating entities. That would be a great opportunity to show that this is just a regular NodeJS code. We don’t have to use bindings! We will start by installing the Azure Tables SDK and then implementing the proper flow to save the upvote.
npm i @azure/data-tables
PowerShellimport { TableClient } from '@azure/data-tables';
import { app, input, output } from '@azure/functions';
import { env } from 'process';
const CONNECTION_STRING_CONFIG_KEY = 'AzureWebJobsStorage';
const UPVOTES_TABLE_NAME = 'upvotes';
const DEFAULT_PARTITION_KEY = 'upvote';
const upvotesInputBinding = input.table({
connection: CONNECTION_STRING_CONFIG_KEY,
tableName: UPVOTES_TABLE_NAME,
partitionKey: DEFAULT_PARTITION_KEY,
rowKey: '{language}',
});
app.post('upvote', {
extraInputs: [upvotesInputBinding],
route: 'upvote/{language}',
handler: (request, context) => {
const [upvotes] = context.extraInputs.get(upvotesInputBinding) as { votes: number }[];
const currentVotes = upvotes?.votes ?? 0;
const upvotesTableClient = TableClient.fromConnectionString(env[CONNECTION_STRING_CONFIG_KEY], UPVOTES_TABLE_NAME);
upvotesTableClient.upsertEntity({
partitionKey: DEFAULT_PARTITION_KEY,
rowKey: request.params.language,
votes: currentVotes + 1,
});
return { status: 201 };
},
});
NodeJS Azure FunctionsThis is the complete simple version of the upvote function. Let’s dig in deeper to what’s happening in the highlighted lines.
Lines 5-7: I’ve moved some of the constants to their own variables so they could be reused
Lines 20-21: Now we get a typed entity and we get only the first item in the resulting array
Lines 23-28: By using the same connection string and the same table name, we get the table client and upsert the entity with +1 upvote
Yes, this is as simple as that! We have our first endpoint that updates a language upvote! The next two functions I will streamline a bit faster, since these are the same concepts.
Create the get-upvotes endpoint (ro)
This should be a straight forward implementation, which is basically the same as the first part of the upvote.
import { TableClient } from '@azure/data-tables';
import { app, input, output } from '@azure/functions';
import { env } from 'process';
const CONNECTION_STRING_CONFIG_KEY = 'AzureWebJobsStorage';
const UPVOTES_TABLE_NAME = 'upvotes';
const DEFAULT_PARTITION_KEY = 'upvote';
const upvotesInputBinding = input.table({
connection: CONNECTION_STRING_CONFIG_KEY,
tableName: UPVOTES_TABLE_NAME,
partitionKey: DEFAULT_PARTITION_KEY,
rowKey: '{language}',
});
app.post('upvote', {
extraInputs: [upvotesInputBinding],
route: 'upvote/{language}',
handler: (request, context) => {
const [upvotes] = context.extraInputs.get(upvotesInputBinding) as { votes: number }[];
const currentVotes = upvotes?.votes ?? 0;
const upvotesTableClient = TableClient.fromConnectionString(env[CONNECTION_STRING_CONFIG_KEY], UPVOTES_TABLE_NAME);
upvotesTableClient.upsertEntity({
partitionKey: DEFAULT_PARTITION_KEY,
rowKey: request.params.language,
votes: currentVotes + 1,
});
return { status: 201 };
},
});
app.get('get-upvotes', {
route: 'upvote/{language}',
extraInputs: [upvotesInputBinding],
handler: (_request, context) => {
const [upvotes] = context.extraInputs.get(upvotesInputBinding) as { votes: number }[];
return { jsonBody: upvotes?.votes ?? 0 };
},
});
NodeJS Azure FunctionsLook how easy it is to create another endpoint! And note line 36, we are reusing the already existing binding to get the data from our storage account!
Adding a queue to the mix as an advanced scenario (vote)
The more seasoned developers might notice the problem with the above approach. If multiple users try to access the function (or even a single user, but in parallel), the upvote
endpoint might face a racing condition, in which two parallel calls will get the same value from the input binding, increase it by one, as save it again. You will have two calls to the API but the value will increase only by 1 point.
There are robust solutions to this problem, but for the sake of learning, let’s just use a simple queue to store “messages” and then handle them one by one in an asynchronous manner. To accomplish this, we will have to change a few things:
- Instead of upserting the votes entity, the
upvote
endpoint will output the upvote to a queue output binding. - Create a new function, triggered by a queue. In that function, do the same logic as in the previous implementation of the
upvote
endpoint:- Get the values by using an input binding
- Upsert the entity using the table SDK
Now this sounds a lot, but it really isn’t! Before you see below the final version of the code, I strongly encourage you to give it a test on your own! If you understand everything that is going above, you will be amazed how easy it is to add yet another function and reuse the same input bindings in it!
Final implementation (fus-to-vote!)
This is the final form of our voting shout. You should be familiar enough with the concepts now to fully understand this without further explanation.
import { TableClient } from '@azure/data-tables';
import { app, input, output } from '@azure/functions';
import { env } from 'process';
const CONNECTION_STRING_CONFIG_KEY = 'AzureWebJobsStorage';
const UPVOTES_TABLE_NAME = 'upvotes';
const DEFAULT_PARTITION_KEY = 'upvote';
const UPVOTES_PROCESSING_QUEUE = 'process-upvotes';
const upvotesInputBinding = input.table({
connection: CONNECTION_STRING_CONFIG_KEY,
tableName: UPVOTES_TABLE_NAME,
partitionKey: DEFAULT_PARTITION_KEY,
rowKey: '{language}',
});
const upvoteProcessingQueueOutput = output.storageQueue({ connection: CONNECTION_STRING_CONFIG_KEY, queueName: UPVOTES_PROCESSING_QUEUE });
type LanguageVotes = { votes: number };
type UpvoteRequest = { language: string };
app.post('upvote', {
extraInputs: [upvotesInputBinding],
extraOutputs: [upvoteProcessingQueueOutput],
route: 'upvote/{language}',
handler: (request, context) => {
const upvoteRequest: UpvoteRequest = { language: request.params.language };
context.extraOutputs.set(upvoteProcessingQueueOutput, upvoteRequest);
return { status: 201 };
},
});
app.storageQueue('process-upvote', {
connection: CONNECTION_STRING_CONFIG_KEY,
queueName: UPVOTES_PROCESSING_QUEUE,
extraInputs: [upvotesInputBinding],
handler: async (message, context) => {
const upvoteRequest = message as UpvoteRequest;
const [upvotes] = context.extraInputs.get(upvotesInputBinding) as LanguageVotes[];
const currentVotes = upvotes?.votes ?? 0;
const upvotesTableClient = TableClient.fromConnectionString(env[CONNECTION_STRING_CONFIG_KEY], UPVOTES_TABLE_NAME);
upvotesTableClient.upsertEntity({
partitionKey: DEFAULT_PARTITION_KEY,
rowKey: upvoteRequest.language,
votes: currentVotes + 1,
});
},
});
app.get('get-upvotes', {
route: 'upvote/{language}',
extraInputs: [upvotesInputBinding],
handler: (_request, context) => {
const [upvotes] = context.extraInputs.get(upvotesInputBinding) as LanguageVotes[];
return { jsonBody: upvotes?.votes ?? 0 };
},
});
NodeJS Azure FunctionsWrap up and final thoughts
Explore the Azure Functions skill tree
As you can see, writing serverless code with the new NodeJS Azure functions v4 model became very easy and super flexible. From here, all you need to do is to explore the different triggers and bindings that exist and how you can combine all of this to create a full blown backend to your system, by creating multiple microservices to handle all of your backend needs.
To explore more, you can always refer back to the official docs:
- Use Azure Functions to develop Node.js serverless solutions (Microsoft Learn)
- Azure Functions Node.js developer guide (Microsoft Learn)
- GA announcement and V3 comparison (Microsoft TechCommunities)
Allow me to leave you with one of my favorite The Dragonborn Comes performances by the amazing Sabina Zweiacker. A great performance to listen to while you code. Happy coding! 🙂