Post

Azure Blob Storage Tutorial for the Node.js Client Library

Azure Blob Storage can be used in a variety of different ways including via the Azure Portal, the Azure CLI and using Azure Blob Storage client libraries that are supported in many different programming languages.

In this tutorial, we will use the Node.js Azure Blob Storage client library to create an App Service Web App written in Javascript to read and write blob files.

This content is available in video form on the Cloud Engineer Skills YouTube channel.

Azure Blob Storage Node.js Client Library Tutorial

Please note this project is not intended to be used in production, it is only offered as a way to teach you how to use the Node.js blob storage client library. There is a security risk running this in production that anyone with access to your API can add new blobs to your storage account and rack up storage and bandwidth related charges.

Keep that in mind when following this tutorial.

Creating Azure Resources for an Azure Blob Storage Account

Login via the Azure CLI using the az login command

1
az login

Create a Resource Group using the az group create command

1
az group create -g cloudengineerskills-sa-rg -l eastus

Creating a Storage Account using the az storage account create command

1
az storage account create -n cloudengineerskillssa -g cloudengineerskills-sa-rg -l eastus --sku Standard_LRS

Creating our Node.js Application

What we will be building is an Node.js API written using the Express framework, that will use the Node.js Blob Storage Client Library to interact with an Azure Blob Storage Account performing the following types of operations.

  • Create a container in a storage account
  • List all containers in a storage account
  • Upload a blob to a container
  • List all blobs in a container
  • Download a blob’s contents
  • Delete a blob
  • Delete a container

Initialising the Project with Dependencies

Initialise the Node.js project with npm init

1
npm init

Install the required dependencies:

1
npm install express dotenv @azure/storage-blob @azure/identity

Updating package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "name": "nodejs-blob-storage-client",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@azure/identity": "^4.4.1",
    "@azure/storage-blob": "^12.24.0",
    "dotenv": "^16.4.5",
    "express": "^4.19.2"
  }
}

Creating the .env file in the root of the project directory

1
AZURE_STORAGE_ACCOUNT_NAME=cloudengineerskillssa

Implementing the Express API using the Node.js Blob Storage Client Library

Use this index.js file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
const express = require("express");
const { BlobServiceClient } = require("@azure/storage-blob");
const { DefaultAzureCredential } = require("@azure/identity");
require("dotenv").config();

const { streamToText } = require("./util.js");

const app_name = "Custom Node.js Express Blobs API";
const app = express();
const port = 8080;
const blobServiceClient = new BlobServiceClient(
  `https://${process.env.AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
  new DefaultAzureCredential()
);

app.use(express.json());

app.get("/", (req, res) => {
  res.send(`Hello from the ${app_name}`);
});

// create a container in a storage account
app.post("/containers", async (req, res) => {
  try {
    const { name } = req.body;
    console.log(`Attempting creation of container with name ${name}`);
    const containerClient = blobServiceClient.getContainerClient(name);
    const createContainer = await containerClient.create();
    const { requestId, date, etag } = createContainer;
    res.send({ name, requestId, date, etag });
  } catch (error) {
    res.status(500).send("Unable to create the container");
  }
});

// list all containers in a storage account
// https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-containers-list-javascript
app.get("/containers", async (req, res) => {
  try {
    console.log(`Attempting listing all containers in blob storage account`);
    let containers = [];

    for await (const containerItem of blobServiceClient.listContainers()) {
      const { name } = containerItem;
      containers.push({ name });
    }

    res.send({ containers });
  } catch (error) {
    res.status(500).send("Unable to list containers");
  }
});

// upload a blob to a container
app.post("/containers/:containerName/blobs", async (req, res) => {
  try {
    const { name, text } = req.body;
    const { containerName } = req.params;
    console.log(
      `Attempting upload of blob with name ${name} and text '${text}' to container ${containerName}`
    );
    const containerClient = blobServiceClient.getContainerClient(containerName);
    const blockBlobClient = containerClient.getBlockBlobClient(name);

    // https://learn.microsoft.com/en-us/javascript/api/@azure/storage-blob/blockblobuploadoptions?view=azure-node-latest
    const uploadBlobOptions = {
      blobHTTPHeaders: { blobContentType: "Content-Type: text/plain" },
      metadata: {
        source: app_name,
      },
    };

    // upload options https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-upload-javascript#upload-data-to-a-block-blob
    const uploadBlobResponse = await blockBlobClient.upload(
      text,
      text.length,
      uploadBlobOptions
    );

    const { requestId, date, etag } = uploadBlobResponse;
    res.send({ name, requestId, date, etag });
  } catch (error) {
    res.status(500).send("Unable to upload blob to container");
  }
});

// list all blobs in a container
app.get("/containers/:containerName/blobs", async (req, res) => {
  try {
    const { containerName } = req.params;
    console.log(`Attempting list all blobs within container ${containerName}`);
    const containerClient = blobServiceClient.getContainerClient(containerName);

    // https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-list-javascript
    let blobs = [];
    for await (const blob of containerClient.listBlobsFlat()) {
      const { name, url } = blob;
      blobs.push({ name, url });
    }

    res.send({ blobs });
  } catch (error) {
    res.status(500).send("Unable to list blobs in the container");
  }
});

// get blob contents
app.get("/containers/:containerName/blobs/:blobName", async (req, res) => {
  try {
    const { containerName, blobName } = req.params;
    console.log(
      `Attempting get blob ${blobName} contents blobs within container ${containerName}`
    );

    const containerClient = blobServiceClient.getContainerClient(containerName);
    const blockBlobClient = containerClient.getBlockBlobClient(blobName);

    const downloadBlockBlobResponse = await blockBlobClient.download(0);
    const data = await streamToText(
      downloadBlockBlobResponse.readableStreamBody
    );

    res.send({ data });
  } catch (error) {
    res.status(500).send("Unable to get blob contents");
  }
});

// delete a blob
app.delete("/containers/:containerName/blobs/:blobName", async (req, res) => {
  try {
    const { containerName, blobName } = req.params;
    console.log(
      `Attempting deletion of blob ${blobName} within container ${containerName}`
    );
    const containerClient = blobServiceClient.getContainerClient(containerName);
    const blockBlobClient = containerClient.getBlockBlobClient(blobName);
    const deleteBlob = await blockBlobClient.delete();
    const { requestId } = deleteBlob;
    res.send({ blobName, requestId });
  } catch (error) {
    res.status(500).send("Unable to delete the blob");
  }
});

// delete a container
app.delete("/containers/:containerName", async (req, res) => {
  try {
    const { containerName } = req.params;
    console.log(`Attempting deletion of container with name ${containerName}`);
    const containerClient = blobServiceClient.getContainerClient(containerName);
    const deleteContainer = await containerClient.delete();
    const { requestId } = deleteContainer;
    res.send({ containerName, requestId });
  } catch (error) {
    res.status(500).send("Unable to delete the container");
  }
});

app.listen(port, () => {
  console.log(`${app_name} app listening on port ${port}`);
});

Along with this util.js file

1
2
3
4
5
6
7
8
9
10
11
12
async function streamToText(readable) {
  readable.setEncoding("utf8");
  let data = "";
  for await (const chunk of readable) {
    data += chunk;
  }
  return data;
}

module.exports = {
  streamToText,
};

Testing it Locally

Login using the Azure CLI with az login and select your account and subscription you wish to use.

1
az login

Next we will grant the Storage Blob Data Contributor Role to the signed in user enabling us to write blobs to the containers within the Azure Storage Account.

1
az ad signed-in-user show --query id -o tsv | az role assignment create --role "Storage Blob Data Contributor" --assignee "@-" --scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>"

You can check in the Azure Portal that this has been signed correctly under My Permissions.

Note that role changes can take up to 10 minutes to take effect.

Run the web application locally with

1
npm run start

You may run various curl command against the application running on http://localhost:8080 to test it is working.

Create a container in a storage account

1
curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "containerabc"}' https://cloudengineerskillsnodeapp.azurewebsites.net/containers

List all containers in a storage account

1
curl http://localhost:8080/containers

Upload a blob to a container

1
curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "blob.txt", "text": "hello world"}' http://localhost:8080/containers/containerabc/blobs

List all blobs in a container

1
curl http://localhost:8080/containers/containerabc/blobs

Download a blob’s contents

1
curl http://localhost:8080/containerabc/blobs/blob.txt

Delete a blob

1
curl -X "DELETE" http://localhost:8080/containerabc/blobs/blob.txt

Delete a container

1
curl -X "DELETE" http://localhost:8080/containerabc

Deploying it to an App Service Web App

Create the App Service Plan

1
az appservice plan create --resource-group cloudengineerskills-sa-rg --name NodeAppPlan --number-of-workers 1 --sku B1 --is-linux --location eastus

Create and Deploy the Web App

1
az webapp up --name CloudEngineerSkillsNodeApp --resource-group cloudengineerskills-sa-rg --plan NodeAppPlan --runtime "NODE:20-lts"

Configure App Settings

Configure the App Setting for the Storage Account

1
az webapp config appsettings set -g cloudengineerskills-sa-rg -n CloudEngineerSkillsNodeApp --settings AZURE_STORAGE_ACCOUNT_NAME=cloudengineerskillssa

Configure the App Setting to forward to port 8080 where the Express Server is listening

1
az webapp config appsettings set -g cloudengineerskills-sa-rg -n CloudEngineerSkillsNodeApp --settings WEBSITES_PORT=8080

Creating a System assigned managed identity

This will provide the App Service Web App CloudEngineerSkillsNodeApp a system assigned managed identity granting the access to the Storage Blob Data Contributor role on the Storage Account.

1
az webapp identity assign --resource-group cloudengineerskills-sa-rg --name CloudEngineerSkillsNodeApp

Replace the <managedIdentityId> below with the principalId output from the az webapp identity assign command, also replace the <subscription-id>, <resource-group-name>, and the <storage-account-name>

1
az role assignment create --assignee "<managedIdentityId>" --role "Storage Blob Data Contributor" --scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Storage/storageAccounts/<storage-account-name>"

Restart the Web App for the App Settings Changes to take effect

1
az webapp restart --name CloudEngineerSkillsNodeApp --resource-group cloudengineerskills-sa-rg

Interacting with a Storage Account via the App Service Web App

Create a container in a storage account

1
curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "containerabc"}' https://cloudengineerskillsnodeapp.azurewebsites.net/containers

List all containers in a storage account

1
curl https://cloudengineerskillsnodeapp.azurewebsites.net/containers

Upload a blob to a container

1
curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "blob.txt", "text": "hello world"}' https://cloudengineerskillsnodeapp.azurewebsites.net/containers/containerabc/blobs

List all blobs in a container

1
curl https://cloudengineerskillsnodeapp.azurewebsites.net/containers/containerabc/blobs

Download a blob’s contents

1
curl https://cloudengineerskillsnodeapp.azurewebsites.net/containers/containerabc/blobs/blob.txt

Delete a blob

1
curl -X "DELETE" https://cloudengineerskillsnodeapp.azurewebsites.net/containers/containerabc/blobs/blob.txt

Delete a container

1
curl -X "DELETE" https://cloudengineerskillsnodeapp.azurewebsites.net/containers/containerabc

Cleaning up the resources

1
az group delete --name cloudengineerskills-sa-rg

Feature Reading

This post is licensed under CC BY 4.0 by the author.