---
cuppy_hub: azure
cuppy_path: guides/storage/access-blobs-from-typescript.md
cuppy_section: docs
cuppy_doc: 343K-DEG4
cuppy_doc_uuid: 2cfe1c50-b192-47b2-bf5b-881d4daa5524
cuppy_permalink: https://cuppy.io/azure/d/343K-DEG4
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/guides/storage/access-blobs-from-typescript
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: e63fb833c413867828773d23adfe3a7ca64b0d540b05f2eb7d583905024960a7
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


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

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.

```bash
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

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

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

## 4. Hand out a link with a user delegation SAS

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

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

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


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
