Getting started with Azure functions v4 + Node.js & TypeScript
Published: 2023-05-02 08:51:54

## 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)

```js
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)

```json
{
  "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.

```js
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)

```js
// 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)

```js
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)

```js
context.res.status(200);

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

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

#### HttpResponse – status (v4)

```js
return { status: 200 };
```

#### Folder structure (v3)

The required folder structure for a JavaScript project in V3 looks like the following:

```txt
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:

```txt
<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`:

```sh
func init MyFunctionApp --model V4
```
The command prompts you to select a runtime and language. Select `node` and `typescript`:

```sh
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` 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:

```json
{
  "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:

```sh
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:

```sh
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:

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

```sh
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:

```ts
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:

```sh
npm install -g azurite
```

You can then start Azurite by running the following command:

```sh
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:

```json
{
  "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.

```sh
func start
```

The following output must appear:

```sh
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:

```sh
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:

```sh
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:

```txt
.
├── dist
│   └── src
│       └── functions
│           └── httpTrigger.js
├── host.json
├── package.json
└── node-modules
```

You can deploy the `deploy.zip` file to Azure using the Azure CLI:

```sh
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:

```sh
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`:

```sh
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:

```ts
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:

```json
{
  "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:

```sh
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:

```ts
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:


![application identity](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iaq0728ob178wtaoj8d.png)

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


![application identity access to the Key Vault](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m57fyjzj6kdmoe1xmxje.png)

### 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:

```ts
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:

```sh
yarn add @azure/cosmos
```

The following function creates a Cosmos DB client and then retrieves a container:

```ts
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:

```ts
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`:

```txt
https://<YOUR-APP-NAME>.scm.azurewebsites.net/DebugConsole
```

To download the ZIP file of the deployed application visit:

```txt
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](https://twitter.com/WolkSoftwareLtd).