DEV Community

Remo H. Jansen for Wolk Software

Posted on

Getting started with Azure functions v4 + Node.js & TypeScript

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}!`
  };
};


Enter fullscreen mode Exit fullscreen mode

HttpTrigger Bindings (v3)



{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}


Enter fullscreen mode Exit fullscreen mode

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}!` };
  }
});


Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

HttpResponse – status (v3)



context.res.status(200);

context.res = { status: 200}
context.res = { statusCode: 200 };

return { status: 200};
return { statusCode: 200 };


Enter fullscreen mode Exit fullscreen mode

HttpResponse – status (v4)



return { status: 200 };


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

⚠️ 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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Enable the v4 programming model

⚠️ During the V4 preview, you must set the app setting AzureWebJobsFeatureFlags to EnableWorkerIndexing.

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": ""
  }
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Create a new function

Navigate into the project folder:



cd MyFunctionApp


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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.


Enter fullscreen mode Exit fullscreen mode

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
});


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

You can then start Azurite by running the following command:



azurite


Enter fullscreen mode Exit fullscreen mode

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!
  }
}


Enter fullscreen mode Exit fullscreen mode

Run your function by starting the local Azure Functions runtime host from the MyFunctionApp folder.



func start


Enter fullscreen mode Exit fullscreen mode

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.


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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 ..


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

The function app settings values can also be read in your code as environment variables:



process.env.CUSTOM_FUNCTION_APP_SETTING


Enter fullscreen mode Exit fullscreen mode

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!
  }
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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);
  }
}


Enter fullscreen mode Exit fullscreen mode

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:

application identity

Then, you need to grant the application identity access to the Key Vault:

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
    }
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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;
}


Enter fullscreen mode Exit fullscreen mode

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; 
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

To download the ZIP file of the deployed application visit:



https://<YOUR-APP-NAME>.scm.azurewebsites.net/api/zip/site/wwwroot/


Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
rcls profile image
OssiDev • Edited

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.

src/
    application/
        errors/
        handlers/
        interfaces/
        services/
        utilities/
    modules/
        sales/
            application/
                actions/
            repositories/
        …
    index.ts
tests/
Enter fullscreen mode Exit fullscreen mode

Each module has a routes.ts file where I can define the paths and handlers.

import { app } from '@azure/functions';
import { deleteSalesData } from '@/modules/sales/application/actions/delete-sales-data';
import { authorizeRequest } from '@/application/handlers/authorization-handler';

app.http('delete-sales-data', {
  route: 'v4/{customerId}/sales/{saleId}/data',
  methods: ['DELETE'],
  authLevel: 'anonymous',
  handler: (request, context) => authorizeRequest(request, context, deleteCategory),
});
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
elglogins profile image
Elgars Logins

How to use shared code in v4?

Collapse
 
gwr profile image
Goh Wei Rong

Before you can run func start, you will need to transpile the code first: npx tsc.