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

Shikhar ShuklaShikhar Shukla
4 min read

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:

  1. Host a static HTML page on Azure Storage with Static Website enabled

  2. Add a button on the page to send data to a backend API

  3. Use Azure Functions as the middleware API layer

  4. Store and retrieve data from Azure Cosmos DB

  5. 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 container

  • Set 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 key

  • Retrieved 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 and getNames) in Azure Function App

  • Copied 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

  1. Opened the Primary Endpoint of Static Website

  2. Entered a name and clicked "Add Name" → Data saved in Cosmos DB

  3. 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.

0
Subscribe to my newsletter

Read articles from Shikhar Shukla directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shikhar Shukla
Shikhar Shukla