● SNIPPETrest· 3 min read
Write to Azure Table Storage from a Node Azure Function — no SDK, SharedKeyLite
When to reach for it: lightweight telemetry, counters, or audit rows from an Azure Function when you don't want @azure/data-tables (and its @azure/core-* tree) bloating a zip-deployed package. The function already has a storage account — AzureWebJobsStorage — so a signed REST call is all you need. ~80 lines, builtin crypto only.
import { createHmac } from 'crypto';
interface TableAccount { account: string; key: Buffer; endpoint: string; }
/** Parse AzureWebJobsStorage. Returns null for dev storage (emulator) — treat as "feature off". */
function parseStorageCs(cs: string): TableAccount | null {
if (!cs || cs.indexOf('UseDevelopmentStorage') !== -1) { return null; }
const parts: Record<string, string> = {};
for (const seg of cs.split(';')) {
const eq = seg.indexOf('='); // AccountKey ends with '==' — split on FIRST '=' only
if (eq > 0) { parts[seg.slice(0, eq).trim()] = seg.slice(eq + 1).trim(); }
}
if (!parts['AccountName'] || !parts['AccountKey']) { return null; }
const suffix = parts['EndpointSuffix'] || 'core.windows.net';
const endpoint = (parts['TableEndpoint']
|| `https://${parts['AccountName']}.table.${suffix}`).replace(/\/+$/, '');
return { account: parts['AccountName'], key: Buffer.from(parts['AccountKey'], 'base64'), endpoint };
}
/** Signed Table service request. resourcePath = '/Tables' | '/MyTable' | '/MyTable()' — NO query string. */
async function tableRequest(
acc: TableAccount, method: 'GET' | 'POST',
resourcePath: string, query: string, body?: string
): Promise<Response> {
const date = new Date().toUTCString();
// SharedKeyLite for TABLE service: Date + "\n" + CanonicalizedResource. That's all.
// (Blob/Queue SharedKeyLite is a DIFFERENT, longer format — don't mix them up.)
const stringToSign = `${date}\n/${acc.account}${resourcePath}`;
const signature = createHmac('sha256', acc.key).update(stringToSign, 'utf8').digest('base64');
const headers: Record<string, string> = {
'Authorization': `SharedKeyLite ${acc.account}:${signature}`,
'x-ms-date': date,
'x-ms-version': '2019-02-02',
'DataServiceVersion': '3.0;NetFx',
'Accept': 'application/json;odata=nometadata'
};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
headers['Prefer'] = 'return-no-content'; // insert returns 204 instead of echoing the entity
}
return fetch(`${acc.endpoint}${resourcePath}${query}`, { method, headers, body });
}
// Create table once (idempotent): 201/204 created, 409 already exists — both fine.
await tableRequest(acc, 'POST', '/Tables', '', JSON.stringify({ TableName: 'MyTelemetry' }));
// Insert an entity:
await tableRequest(acc, 'POST', '/MyTelemetry', '', JSON.stringify({
PartitionKey: '202608', // e.g. YYYYMM for monthly partitions
RowKey: `${Date.now()}_${crypto.randomUUID()}`, // unique + roughly time-ordered
count: 1
}));
// Query a partition (paged):
const res = await tableRequest(acc, 'GET', '/MyTelemetry()',
`?$filter=${encodeURIComponent(`PartitionKey eq '202608'`)}`);
const rows = (await res.json()).value;
// continuation: response headers x-ms-continuation-NextPartitionKey / -NextRowKey →
// re-issue with &NextPartitionKey=...&NextRowKey=... until the header disappears.
Key points:
- Table SharedKeyLite ≠ Blob SharedKeyLite. The Table string-to-sign is just
Date\n/{account}{path}; the Blob/Queue variant includes method, headers and more. Using the Blob format against Table gets you an eternal 403 with no useful detail. - The query string is NOT signed for Table SharedKeyLite (no
comp=in normal CRUD) — sign the resource path only, send$filteretc. unsigned. - Split the connection string on the first
=per segment — the base64AccountKeyends in==and a naivesplit('=')corrupts it. RowKey/PartitionKeyforbidden characters:/ \ # ?— timestamps + GUIDs are safe.- Numbers serialize as
Edm.Int32/Edm.Doubleautomatically; integers within int32 range need noodata.typeannotation. - Wrap writes in try/catch with a short
AbortControllertimeout if the row is telemetry — a metering hiccup must never take down the request that produced it (fail-open). - Works against sovereign clouds too: honor
EndpointSuffix/ explicitTableEndpointinstead of hard-codingcore.windows.net.
Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub