In this post, we are going to take a look at everything you need to know to get started with Azure Functions v4 (currently in preview). We will also take a look at some common use cases.
What are Azure functions?
An Azure Function App is a serverless compute service provided by Microsoft Azure, which allows developers to easily create and deploy small pieces of code, known as "functions," that can be executed in response to events or triggers. Azure Function Apps can be written in a variety of languages, including C#, F#, JavaScript, PowerShell, Python, and TypeScript.
The main advantage of Azure Function Apps over traditional API apps is that they are fully managed and scalable, meaning developers can focus on writing the code for the specific task at hand, without worrying about the underlying infrastructure or server management. Azure Function Apps are also pay-per-use, which means that developers only pay for the resources they consume while their functions are running, and they don't have to worry about paying for idle time or unused resources.
Azure Function Apps is that they can be integrated with a wide range of other Azure services, such as Azure Storage, Azure Event Hubs, Azure Service Bus, and Azure Cosmos DB, making it easy to build complex applications that leverage multiple Azure services. Additionally, Azure Function Apps can be used to build event-driven architectures, where functions can be triggered by events such as changes to a database, the arrival of a message in a queue, or the upload of a file to a storage account. This allows for highly scalable and reactive applications that can respond to changes in real-time.
What is exciting about Azure functions v4?
Version 4 was designed with the following goals in mind:
- Provide a familiar and intuitive experience to Node.js developers
- Make the file structure flexible with support for full customization
- Switch to a code-centric approach for defining function configuration
The Node.js "programming model" shouldn't be confused with the Azure Functions "runtime".
Programming model: Defines how you author your code and is specific to JavaScript and TypeScript.
Runtime: Defines underlying behavior of Azure Functions and is shared across all languages.The programming model version is strictly tied to the version of the @azure/functions npm package, and is versioned independently of the runtime. Both the runtime and the programming model use "4" as their latest major version, but that is purely a coincidence.
The V4 model uses an app object as the entry point for registering functions instead of function.json
files. For example, to register an HTTP trigger responding to a GET request, you can call app.http()
or app.get()
which was modeled after other Node.js frameworks like Express.js that also support app.get()
.
HttpTrigger Handler (v3)
module.exports = async function (context, req) {
context.log('HTTP function processed a request');
const name = req.query.name
|| req.body
|| 'world';
context.res = {
body: `Hello, ${name}!`
};
};
HttpTrigger Bindings (v3)
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
HttpTrigger Handler + Bindings (v4)
Trigger configuration like methods and authLevel that were specified in a function.json
file before are moved to the code itself in V4. V4 also sets several defaults for you, which is why you don't see authLevel
or an output
binding in the V4 example.
const { app } = require("@azure/functions");
app.http('helloWorld1', {
methods: ['GET', 'POST'],
handler: async (request, context) => {
context.log('Http function processed request');
const name = request.query.get('name')
|| await request.text()
|| 'world';
return { body: `Hello, ${name}!` };
}
});
The V4 model, have adjusted the HTTP request and response types to be a subset of the fetch standard instead of types unique to Azure Functions. V4 uses Node.js's undici package, which follows the fetch standard and is currently being integrated into Node.js core.
HttpRequest - body (v3)
// returns a string, object, or Buffer
const body = request.body;
// returns a string
const body = request.rawBody;
// returns a Buffer
const body = request.bufferBody;
// returns an object representing a form
const body = await request.parseFormBody();
HttpRequest - body (v4)
const body = await request.text();
const body = await request.json();
const body = await request.formData();
const body = await request.arrayBuffer();
const body = await request.blob();
HttpResponse – status (v3)
context.res.status(200);
context.res = { status: 200}
context.res = { statusCode: 200 };
return { status: 200};
return { statusCode: 200 };
HttpResponse – status (v4)
return { status: 200 };
Folder structure (v3)
The required folder structure for a JavaScript project in V3 looks like the following:
FunctionsProject
| - MyFirstFunction
| | - index.js
| | - function.json
| - MySecondFunction
| | - index.js
| | - function.json
| - SharedCode
| | - myFirstHelperFunction.js
| | - mySecondHelperFunction.js
| - node_modules
| - host.json
| - package.json
Folder structure (v4)
The recommended folder structure for a JavaScript project in V4 looks like the following:
<project_root>/
| - .vscode/
| - src/
| | - functions/
| | | - myFirstFunction.js
| | | - mySecondFunction.js
| - test/
| | - functions/
| | | - myFirstFunction.test.js
| | | - mySecondFunction.test.js
| - .funcignore
| - host.json
| - local.settings.json
| - package.json
⚠️ Keep in mind that you can't mix the v3 and v4 programming models in the same function app. As soon as you register one v4 function in your app, any v3 functions registered in function.json files are ignored.
Create a new Azure function v4 project
The version 4 of the Node.js programming model requires the following minimum versions:
- @azure/functions npm package v4.0.0-alpha.9+
- Node.js v18+
- TypeScript v4+
- Azure Functions Runtime v4.16+
- Azure Functions Core Tools v4.0.5095+
In Azure Functions, a function project is a container for one or more individual functions that each responds to a specific trigger. All functions in a project share the same local and hosting configurations.
Run the func init
command, as follows, to create a functions project in a folder named MyFunctionApp
:
func init MyFunctionApp --model V4
The command prompts you to select a runtime and language. Select node
and typescript
:
Select a number for worker runtime:
1. dotnet
2. dotnet (isolated process)
3. node
4. python
5. powershell
6. custom
Choose option: 3
node
Select a number for language:
1. javascript
2. typescript
Choose option: 2
typescript
Enable the v4 programming model
⚠️ During the V4 preview, you must set the app setting
AzureWebJobsFeatureFlags
toEnableWorkerIndexing
.
The following application setting is required to run the v4 programming model while it is in preview:
- Name:
AzureWebJobsFeatureFlags
- Value:
EnableWorkerIndexing
If you're running locally using Azure Functions Core Tools, you should add this setting to your local.settings.json
file:
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing", // <-- HERE!
"AzureWebJobsStorage": ""
}
}
If you're running in Azure, follow these steps with the tool of your choice:
az functionapp config appsettings set --name <FUNCTION_APP_NAME> \
--resource-group <RESOURCE_GROUP_NAME> \
--settings AzureWebJobsFeatureFlags=EnableWorkerIndexing
Create a new function
Navigate into the project folder:
cd MyFunctionApp
This folder contains various files for the project, including configurations files named local.settings.json and host.json. Because local.settings.json can contain secrets downloaded from Azure, the file is excluded from source control by default in the .gitignore file.
Add a function to your project by using the following command:
func new
Choose the template for HTTP trigger
. You can keep the default name (httpTrigger
) or give it a new name (HttpExample
). Your function name must be unique, or you're asked to confirm if your intention is to replace an existing function. You can find the function you added in the src/functions
directory.
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Durable Functions entity
4. Durable Functions orchestrator
5. Azure Event Grid trigger
6. Azure Event Hub trigger
7. HTTP trigger
8. Azure Queue Storage trigger
9. Azure Service Bus Queue trigger
10. Azure Service Bus Topic trigger
11. Timer trigger
Choose option: 7
HTTP trigger
Function name: [httpTrigger]
Creating a new file /src/functions/httpTrigger.ts
The function "httpTrigger" was created successfully from the "HTTP trigger" template.
The function will be created under /src/functions/httpTrigger.ts
as follows:
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
export async function httpTrigger(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`Http function processed request for url "${request.url}"`);
const name = request.query.get('name') || await request.text() || 'world';
return { body: `Hello, ${name}!` };
};
app.http('httpTrigger', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: httpTrigger
});
We now need to add Azure Storage connection information in local.settings.json
. If you don't have an Azure Storage account, you can use the Azurite to run a local storage emulator. You can install Azurite using the following command:
npm install -g azurite
You can then start Azurite by running the following command:
azurite
The easiest way to connect to the emulator from your application is to configure a connection string in your application's configuration file that references the shortcut UseDevelopmentStorage=true
. The shortcut is equivalent to the full connection string for the emulator. In your Azure Functions project, open the local.settings.json
file and update the connection string for your storage account:
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
"AzureWebJobsStorage": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://127.0.0.1:10000" // <-- HERE!
}
}
Run your function by starting the local Azure Functions runtime host from the MyFunctionApp
folder.
func start
The following output must appear:
Azure Functions Core Tools
Core Tools Version: 4.0.5095 Commit hash: N/A (64-bit)
Function Runtime Version: 4.16.5.20396
[2023-04-14T19:50:22.069Z] Worker process started and initialized.
...
Functions:
httpTrigger: [GET,POST] http://localhost:7071/api/httpTrigger
For detailed output, run func with --verbose flag.
Deploying your function app to Azure
First, you need to build your function app. This will create a dist
folder with the compiled JavaScript files:
yarn build
Then, you need to create a ZIP file with the content of the dist
folder, and the host.json
and package.json
files. You can use the following commands to do so:
rm -r ./dist/**/*.map
cp -r ./host.json ./package.json ./node_modules ./dist
rm -r ./dist/node_modules/@types/
rm -r ./dist/node_modules/azure-functions-core-tools/
rm -r ./dist/node_modules/typescript/
cd ./dist
mkdir dist
mv ./src ./dist/
zip -r ../deploy.zip ./*
cd ..
After running these commands, you should have a deploy.zip
file in your project folder, with the following contents:
.
├── dist
│ └── src
│ └── functions
│ └── httpTrigger.js
├── host.json
├── package.json
└── node-modules
You can deploy the deploy.zip
file to Azure using the Azure CLI:
az login
az account set --subscription YOUR-AZURE-SUBSCRIPTION-ID
az functionapp deployment source config-zip -g YOUR-RESOURCE-GROUP -n YOUR-FUNCTION-APP --src ./deploy.zip
Recipe 1: Working with environment variables
The az functionapp config appsettings list
command returns the existing application settings, as in the following example:
az functionapp config appsettings list --name <FUNCTION_APP_NAME> \
--resource-group <RESOURCE_GROUP_NAME>
The az functionapp config appsettings set
command adds or updates an application setting. The following example creates a setting with a key named CUSTOM_FUNCTION_APP_SETTING
and a value of 12345
:
az functionapp config appsettings set --name <FUNCTION_APP_NAME> \
--resource-group <RESOURCE_GROUP_NAME> \
--settings CUSTOM_FUNCTION_APP_SETTING=12345
The function app settings values can also be read in your code as environment variables:
process.env.CUSTOM_FUNCTION_APP_SETTING
When run develop a function app locally, you must maintain local copies of these values in the local.settings.json
project file:
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
"AzureWebJobsStorage": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://127.0.0.1:10000",
"CUSTOM_FUNCTION_APP_SETTING": 12345 // <-- HERE!
}
}
Recipe 2: Accessing Azure resources from your function
If your application access other resources in Azure, you will need to create an application identity and grant it access to those resources. For example, you might want to access a Key Vault to retrieve secrets. You can do so by using the @azure/identity
and @azure/keyvault-secrets
packages:
yarn add @azure/identity @azure/keyvault-secrets
Then, you can write a function that retrieves a secret from Key Vault. The following example uses the DefaultAzureCredential
class to authenticate with Azure Active Directory and then retrieves a secret from Key Vault:
import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
interface AppSecrets {
// your secrets here
}
export async function getSecrets(): Promise<AppSecrets> {
const keyVaultName = "your-key-vault-name";
const KVUri = `https://${keyVaultName}.vault.azure.net`;
const secretName = "your-secret-name";
const credential = new DefaultAzureCredential(); // <-- HERE!
const client = new SecretClient(KVUri, credential);
const retrievedSecret = await client.getSecret(secretName);
const value = retrievedSecret.value;
if (value === undefined) {
throw new Error();
} else {
return JSON.parse(value);
}
}
If you run the previous code in an Azure function, it will fail because you need to create an application identity first. You can do so in the Azure portal:
Then, you need to grant the application identity access to the Key Vault:
Recipe 3: Working with Cosmos DB
You can use the @azure/cosmos
package to access Cosmos DB from your function app. You could store your Cosmos DB connection string in the application settings, but it is better to store it in Key Vault and retrieve it using the getSecrets
function from the previous recipe.
The following function retrieves the Cosmos DB connection string from Key Vault and then uses it to create a Cosmos DB configuration object:
import { getSecrets } from "./secrets";
export async function getDatabaseConfig(containerId: string, log: (txt: string) => void) {
log('Invoking getDatabaseConfig...');
const appSecrets = await getSecrets(log);
return {
endpoint: appSecrets.cosmosEndpoint,
key: appSecrets.cosmosKey,
databaseId: "wolkdotcomui",
containerId: containerId
}
}
You can then use the getDatabaseConfig
function to create a Cosmos DB client and retrieve a container. You will need to install the @azure/cosmos
package:
yarn add @azure/cosmos
The following function creates a Cosmos DB client and then retrieves a container:
import { Container, CosmosClient } from "@azure/cosmos";
import { getDatabaseConfig } from "./config";
export async function getDbContainer(containerId: string, log: (txt: string) => void): Promise<Container> {
log(`Invoking getDbContainer... ${containerId}`);
const dbConfig = await getDatabaseConfig(containerId, log);
const { endpoint, key, databaseId } = dbConfig;
const client = new CosmosClient({
endpoint,
key
});
const database = client.database(databaseId);
const container = database.container(containerId);
return container;
}
The container can then be used to retrieve items from Cosmos DB. For example, the following function retrieves a user from a container named users
in Cosmos DB:
import { getDbContainer } from "../shared/db";
export interface User {
id: string;
name: string;
email: string;
}
export async function getUserById(id: string): Promise<User> {
const containerId = 'users';
const dbContainer = await getDbContainer(containerId, log);
const item = dbContainer.item(id, id);
const response = await item.read<User>();
if (response.resource) {
throw new Error('User not found');
}
return response.resource;
}
You can then use the getUserById
in your functions.
Recipe 4: Troubleshooting deployments
If after deploying your function app your application is not working you should head to the azure portal and check a few things:
- Check the logs in the
Log stream
section of the portal. - Try to restart the function app from the
Overview
section. - Visit the
App Service Editor (Preview)
section of the portal to see the deployed files. - Visit the
Functions
section of the portal to see the deployed functions.
You can use the follwing URL to access the Debug Console
:
https://<YOUR-APP-NAME>.scm.azurewebsites.net/DebugConsole
To download the ZIP file of the deployed application visit:
https://<YOUR-APP-NAME>.scm.azurewebsites.net/api/zip/site/wwwroot/
We hope that this post has been useful. If you have any questions or comments, please contact us via Twitter at @WolkSoftwareLtd.
Top comments (3)
Good, comprehensive post! I was about to write a similar one after I started my migration from a previous programming model to v4, which was made generally available in September, 2023. I have a back-end application in Azure Functions that has about 80 functions atm.
I didn't just want to move the v3 folder structure inside
src/functions/
, but instead modularize my code (which I can now do!). I wrote a singular entry point (src/index.ts
) that imports all routes from modules and I keep my application layer stuff separate.Each module has a
routes.ts
file where I can define the paths and handlers.In here I can also add middleware if needed.
The one tip I'll share is if you use TypeScript and absolute paths: install
tsc-alias
so it'll change your absolute paths to relative paths, while also adding file extensions. I have no idea why the new runtime can't find files without file extensions, but that's what happened to me.How to use shared code in v4?
Before you can run
func start
, you will need to transpile the code first:npx tsc
.