Read and write Blob and Data Lake storage from TypeScript with Entra ID

Use @azure/storage-blob and @azure/storage-file-datalake from Node.js without account keys — BlobServiceClient with DefaultAzureCredential or ManagedIdentityCredential, the data role to assign, idempotent container creation, uploads with content type, ETag-conditional writes, reading the storage error code from RestError (details.errorCode vs code), the SDK's retry defaults, a user delegation SAS for handing out a link, and the Data Lake client for directories and ACLs.

markdownguidev1343K-DEG4stage: buildtrust: officialservice: storageinterface: sdklanguages: typescriptBlobServiceClient DefaultAzureCredential@azure/storage-blob@azure/storage-file-datalakeDataLakeServiceClientContainerClient createIfNotExistsBlockBlobClient uploaduploadDatadownloadToBufferblobContentTypeconditions ifMatch etagifNoneMatch "*"isRestErrordetails.errorCodex-ms-error-codeStorageRetryOptions maxTriesgetUserDelegationKeygenerateUserDelegationSasUrlBlobSASPermissionsSASProtocol.Httpsconnection string avoidAZURE_STORAGE_CONNECTION_STRINGstorage TypeScript example

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

Tested types: @azure/storage-blob 12.34.0, @azure/storage-file-datalake 12.31.0, @azure/identity 4.14.0, @azure/core-rest-pipeline 1.25.0, TypeScript strict, Node.js ESM.

1. Give the identity a data role

The identity that runs the code (your user locally, the app's managed identity in Azure) needs Storage Blob Data Contributor (or Reader) on the account or container. Owner and Contributor aren't enough (reference/storage/access-models.md). Allow 10–30 minutes after assigning.

az role assignment create --assignee-object-id <principal-id> --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" --scope <storage-account-resource-id>

Use --assignee-principal-type User for yourself. Don't use connection strings or AZURE_STORAGE_CONNECTION_STRING: they carry the account key and stop working when Shared Key is turned off.

2. Create the client and write

import { DefaultAzureCredential } from "@azure/identity";
import { BlobServiceClient } from "@azure/storage-blob";

const account = process.env.STORAGE_ACCOUNT_NAME;
if (!account) throw new Error("STORAGE_ACCOUNT_NAME is not set");

// One client per process: it pools connections and caches tokens.
const credential = new DefaultAzureCredential();
const service = new BlobServiceClient(`https://${account}.blob.core.windows.net`, credential, {
  retryOptions: { maxTries: 5 }, // default 4 tries, exponential from 4 s, capped at 120 s
});

const container = service.getContainerClient("orders");
await container.createIfNotExists(); // no public access level: never send one

const blob = container.getBlockBlobClient("2026/10/order-1.json");
const body = JSON.stringify({ id: 1, status: "new" });
await blob.upload(body, Buffer.byteLength(body), {
  blobHTTPHeaders: { blobContentType: "application/json" },
});

const bytes = await blob.downloadToBuffer();
console.log(JSON.parse(bytes.toString("utf8")));
  • In Azure, prefer new ManagedIdentityCredential({ clientId }) for a user-assigned identity, or set AZURE_CLIENT_ID for DefaultAzureCredential (reference/azure-identity/credential-chain.md).
  • For files and streams use blob.uploadFile(path) or blob.uploadStream(stream): they stage blocks in parallel and stay under the block limits.
  • Blob names are case-sensitive and may contain /; there are no real folders unless the account has a hierarchical namespace.

3. Conditional writes and error codes

import { isRestError } from "@azure/core-rest-pipeline";
import type { BlockBlobClient } from "@azure/storage-blob";

/** The storage error code: header x-ms-error-code (present even on HEAD), else the XML body's Code. */
export function storageErrorCode(e: unknown): string | undefined {
  if (!isRestError(e)) return undefined;
  const details = e.details as { errorCode?: string } | undefined;
  return details?.errorCode ?? e.code;
}

/** Create only if absent; returns false if it already existed. */
export async function createOnce(blob: BlockBlobClient, body: string): Promise<boolean> {
  try {
    await blob.upload(body, Buffer.byteLength(body), { conditions: { ifNoneMatch: "*" } });
    return true;
  } catch (e) {
    if (storageErrorCode(e) === "BlobAlreadyExists") return false;
    throw e;
  }
}

/** Optimistic update: fails with ConditionNotMet (412) if someone wrote in between. */
export async function updateIfUnchanged(blob: BlockBlobClient, body: string): Promise<void> {
  const { etag } = await blob.getProperties();
  try {
    await blob.upload(body, Buffer.byteLength(body), { conditions: { ifMatch: etag } });
  } catch (e) {
    if (storageErrorCode(e) === "ConditionNotMet") {
      throw new Error("Blob changed since it was read: re-read, merge and retry", { cause: e });
    }
    throw e;
  }
}

e.statusCode gives the HTTP status. Codes to branch on: AuthorizationPermissionMismatch (role), KeyBasedAuthenticationNotPermitted (you're using a key), AuthorizationFailure (network), ContainerNotFound, BlobNotFound, ConditionNotMet. The full lists: reference/storage/access-errors.md, reference/storage/sas-and-key-errors.md, reference/storage/data-errors.md. Don't retry 403s: the SDK already retries 500, 503 and network errors such as ETIMEDOUT.

The caller needs generateUserDelegationKey (Storage Blob Delegator, or Data Contributor/Owner). The SAS can't outlive the key (≤ 7 days).

import { DefaultAzureCredential } from "@azure/identity";
import { BlobSASPermissions, BlobServiceClient, SASProtocol } from "@azure/storage-blob";

const service = new BlobServiceClient(
  `https://${process.env.STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
  new DefaultAzureCredential(),
);
const blob = service.getContainerClient("orders").getBlobClient("2026/10/order-1.json");

const startsOn = new Date(Date.now() - 15 * 60 * 1000); // allow for clock skew
const expiresOn = new Date(Date.now() + 60 * 60 * 1000); // one hour
const key = await service.getUserDelegationKey(startsOn, expiresOn);

const url = await blob.generateUserDelegationSasUrl(
  { permissions: BlobSASPermissions.parse("r"), startsOn, expiresOn, protocol: SASProtocol.Https },
  key,
);
// Treat url as a secret: don't log it.

To bind the SAS to one Entra user (service version 2025-07-05+), add delegatedUserObjectId to the options; the holder must then also present that user's token (reference/storage/sas.md).

5. Directories and ACLs (hierarchical namespace)

import { DefaultAzureCredential } from "@azure/identity";
import { DataLakeServiceClient } from "@azure/storage-file-datalake";

const lake = new DataLakeServiceClient(
  `https://${process.env.STORAGE_ACCOUNT_NAME}.dfs.core.windows.net`,
  new DefaultAzureCredential(),
);
const dir = lake.getFileSystemClient("raw").getDirectoryClient("landing/2026");
await dir.createIfNotExists();

const file = dir.getFileClient("events.json");
const data = Buffer.from('{"ok":true}');
await file.upload(data);

const acl = await dir.getAccessControl();
console.log(acl.owner, acl.acl.length);

Writes from the Data Lake client and the Blob client to the same file conflict (BlobOperationNotSupported, InvalidFlushOperation); pick one per file. ACL rules: reference/storage/adls-gen2.md.

Next

The full app wiring (identity, role, network): patterns/app-to-storage-with-managed-identity.md.

Read this document as markdown — the same bytes an agent receives.