Learn Azure Blob Storage Media Library Using Angular and Azure Functions
AI links open with a title + excerpt (these tools can't fetch the page themselves) — use "Copy full article" to paste the complete text for a fuller summary.
Introduction
This article demonstrates how to build a media library using Azure Blob Storage, Azure Functions and Angular.
When we moved our blog from WordPress, the posts came but the images did not. Every image still pointed to the old site. So we needed our own place to store them, and an admin page to manage them.
Azure Blob Storage
Blob Storage keeps files, and it also keeps metadata on every file. Metadata is a simple key and value list, like originalName and altText.
This is useful. Normally you create a media table in your database to store the file name, size and alt text. Here we do not need that table. The blob itself holds everything.
Features
- Upload image, PDF or text file.
- Replace a file and keep the same URL.
- Edit alt text without re-uploading.
- Delete a file.
- No database table for the file list.
With the following steps, you can build the media library.
- Create the storage account and container.
- Configure the connection in the API.
- Upload the file.
- Replace the file and fix the browser cache.
- List the files in the admin page.
Create the Storage Account and Container
Open the Azure Portal. Go to Storage accounts and click Create.
After the account is ready, open it and select Containers in the left side, then click + Container. Enter the name images.
Set the public access level to Blob. This makes the image readable by anyone with the URL, which is what we want for a blog image. The container itself stays private, so nobody can list your files.
Now go to Access keys and copy the connection string.
Configure the Connection
Add the connection string to api/local.settings.json for local development, and to the Azure Static Web Apps Application Settings for production.
The client is created only once and kept in memory. Azure Functions reuses the same instance for many requests, so there is no need to build it every time.
// api/src/blobStorage.ts
let _containerClient: ContainerClient | null = null;
export function getContainerClient(): ContainerClient {
if (!_containerClient) {
const serviceClient = BlobServiceClient.fromConnectionString(connectionString!);
_containerClient = serviceClient.getContainerClient(containerName);
}
return _containerClient;
}
Note there is no createIfNotExists here. We already created the container in the portal. Calling it on every request is one extra network trip for nothing.
Upload the File
The upload endpoint is POST /api/media. Check the file first, then send it to Azure.
const ext = ALLOWED_CONTENT_TYPES[file.type];
if (!ext) return { status: 400, jsonBody: { error: 'Unsupported file type' } };
if (file.size > MAX_FILE_SIZE_BYTES) return { status: 400, jsonBody: { error: 'File exceeds the 5MB size limit.' } };
ALLOWED_CONTENT_TYPES is a small map from the content type to the extension — image/jpeg to jpg, image/png to png, and so on. It does two jobs. It blocks any type not in the list, and it gives us the extension.
My best suggestion is to never use the file name sent by the browser. Generate your own name.
const name = `${randomUUID()}.${ext}`;
A GUID name removes many problems at one time. No ../ in the path, no two users overwriting the same logo.png, no strange characters breaking the URL.
The original name is not lost. It goes into the metadata.
await blockBlobClient.uploadData(buffer, {
blobHTTPHeaders: { blobContentType: file.type },
metadata: {
originalName: encodeURIComponent(file.name),
altText: encodeURIComponent(altText),
uploadedAt: now,
updatedAt: now,
},
});
Important: blob metadata travels as an HTTP header, so it allows ASCII only. If your file name has an accent, or your alt text has a long dash, the upload will fail. Use encodeURIComponent when you write and decode when you read.
Replace the File and Fix the Cache
PUT /api/media/{name} uploads the new file to the same blob name. Every blog post that already uses that URL now shows the new image, and you edit nothing.
But there is one problem. The URL did not change, so the browser shows the old image from its cache.
The fix is a small version number taken from the blob's own updated time.
export function buildPublicUrl(blobUrl: string, updatedAtMs: number): string {
return `${blobUrl}?v=${updatedAtMs}`;
}
The URL stays the same while the file is the same, so caching still works. It changes the moment you replace the file.
List the Files in the Admin Page
for await (const blob of containerClient.listBlobsFlat({ includeMetadata: true })) { ... }
Do not forget includeMetadata: true. Without it you must call getProperties() for every blob, and one list becomes a hundred requests.
Now you can upload an image here and copy the URL into any blog post.
Run It Locally
You do not need a real storage account to develop. Azurite is the Microsoft storage emulator and it works with the same code and the same SDK.
npm run start:azurite
Point AZURE_STORAGE_CONNECTION_STRING to Azurite locally, and to the real account in Azure. No code change.
Conclusion
In this article we learned how to use Azure Blob Storage as a media library, with metadata instead of a database table, GUID file names, and a version number in the URL to beat the browser cache.
Reference
Comments
Be the first to comment.