Building a UI + Middleware + Backend Workflow on Azure with Static Website, Function Apps, and Cosmos DB

Today, I explored a complete Azure-based workflow that connects a frontend UI, middleware with Azure Functions, and a backend database using Cosmos DB — all integrated into a working application.
This guide breaks down what I built and how you can recreate it.
1. The Project Overview
The goal was to:
Host a static HTML page on Azure Storage with Static Website enabled
Add a button on the page to send data to a backend API
Use Azure Functions as the middleware API layer
Store and retrieve data from Azure Cosmos DB
Test the full flow end-to-end
2. Hosting the Static Website
Steps:
Created a Storage Account
Enabled Static Website from the settings
Uploaded an
index.html
file to the$web
containerSet index.html as the Index Document Name
index.html Example:
<!DOCTYPE html>
<html>
<head>
<title>Azure Function Trigger</title>
</head>
<body>
<h1>Hello from Static Website!</h1>
<input type="text" id="nameInput" placeholder="Enter name" />
<button onclick="addName()">Add Name</button>
<button onclick="getNames()">Get Names</button>
<script>
const addUrl = "YOUR_ADDNAME_FUNCTION_URL";
const getUrl = "YOUR_GETNAMES_FUNCTION_URL";
function addName() {
const name = document.getElementById("nameInput").value;
fetch(addUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name })
})
.then(res => res.json())
.then(data => alert(data.message || "Done"));
}
function getNames() {
fetch(getUrl)
.then(res => res.json())
.then(data => console.log(data));
}
</script>
</body>
</html>
3. Setting up Cosmos DB
Created a Cosmos DB Account (Core SQL API)
Created a Database named
NamesDB
Created a Container named
names
with/id
as the partition keyRetrieved COSMOS_DB_ENDPOINT and COSMOS_DB_KEY from Keys section
4. Creating Azure Functions
a) addName Function
Handles inserting new names into Cosmos DB.
index.js
:
const { CosmosClient } = require("@azure/cosmos");
const client = new CosmosClient({ endpoint: process.env.COSMOS_DB_ENDPOINT, key: process.env.COSMOS_DB_KEY });
const container = client.database("NamesDB").container("names");
module.exports = async function (context, req) {
if (req.method === "OPTIONS") {
context.res = { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" } };
return;
}
const name = req.body?.name;
if (!name) {
context.res = { status: 400, body: { error: "Missing 'name'" }, headers: { "Access-Control-Allow-Origin": "*" } };
return;
}
try {
const item = { id: Date.now().toString(), name, createdAt: new Date().toISOString() };
const { resource } = await container.items.create(item);
context.res = { status: 200, body: { message: "Name added", name: resource.name }, headers: { "Access-Control-Allow-Origin": "*" } };
} catch (err) {
context.res = { status: 500, body: { error: err.message }, headers: { "Access-Control-Allow-Origin": "*" } };
}
};
function.json
:
{
"bindings": [
{ "authLevel": "function", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["post", "options"] },
{ "type": "http", "direction": "out", "name": "res" }
]
}
b) getNames Function
Fetches all names from Cosmos DB.
index.js
:
const { CosmosClient } = require("@azure/cosmos");
const client = new CosmosClient({ endpoint: process.env.COSMOS_DB_ENDPOINT, key: process.env.COSMOS_DB_KEY });
const container = client.database("NamesDB").container("names");
module.exports = async function (context, req) {
if (req.method === "OPTIONS") {
context.res = { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" } };
return;
}
try {
const query = "SELECT * FROM c ORDER BY c.createdAt DESC";
const { resources } = await container.items.query(query).fetchAll();
context.res = { status: 200, body: resources, headers: { "Access-Control-Allow-Origin": "*" } };
} catch (err) {
context.res = { status: 500, body: { error: err.message }, headers: { "Access-Control-Allow-Origin": "*" } };
}
};
function.json
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "options"]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "index.js"
}
5. Connecting the Frontend and Backend
Deployed both functions (
addName
andgetNames
) in Azure Function AppCopied the Function URLs and pasted them into the HTML file
Uploaded the updated HTML file back to
$web
container in Storage Account
6. Testing the Workflow
Opened the Primary Endpoint of Static Website
Entered a name and clicked "Add Name" → Data saved in Cosmos DB
Clicked "Get Names" → Retrieved data from Cosmos DB and printed in console
7. Cleanup to Avoid Billing
Deleted Cosmos DB, Function App, and Storage Account after testing
Best practice: Always create resources in a dedicated Resource Group so you can delete the whole group in one click
Final Thoughts
This exercise showed how easily Azure services can be connected to build a full-stack cloud app:
Static Website for UI
Function App for API layer
Cosmos DB for data storage
The same approach can be extended for real-world apps like guest books, form submissions, IoT dashboards, and more.
Subscribe to my newsletter
Read articles from Shikhar Shukla directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
