Learn Azure Cosmos DB Using Azure Functions and TypeScript

Sep 17, 2026AzureDatabasesEngineering
Summarize with AI: Google AI Claude ChatGPT Perplexity Grok

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.

Share:

Introduction

This article demonstrates how to use Azure Cosmos DB from an Azure Functions v4 API written in TypeScript.

Cosmos DB is the main database for our site. The blog posts, the comments and a general items collection all live in one database called blog-db.

Azure Cosmos DB

Cosmos DB is a NoSQL document database. You store JSON documents in a container, and every container needs a partition key.

Features

  • Point read by id is very fast and very cheap.
  • SQL style query language on JSON.
  • Automatic index on every field.
  • Cost is measured in RU (Request Units), not in time.

With the following steps, you can connect Cosmos DB to your API.

  1. Create the account, database and container.
  2. Create the client one time only.
  3. Choose the partition key.
  4. Query the list.
  5. Hide the draft posts.

Create the Account, Database and Container

Open the Azure Portal, search Azure Cosmos DB and click Create. Select the Azure Cosmos DB for NoSQL option.

Create an Azure Cosmos DB for NoSQL account

After the account is ready, open Data Explorer and create a new database with the name blog-db, then add a container.

Create the database and container in Data Explorer

Now go to Keys in the left side and copy the primary connection string.

Copy the primary connection string from Keys

Put it in api/local.settings.json for local work, and in the Azure Static Web Apps Application Settings for production. Never commit the real value.

Create the Client One Time Only

This is the most common mistake with Cosmos DB in a Function App. Do not create the CosmosClient inside your handler.

The client holds a connection pool. If you build it on every request, you pay a new TCP and TLS handshake each time, and under load you will run out of ports.

Azure Functions keeps the same instance warm for many requests, so anything stored at module level stays alive. Create the client once and keep it.

// api/src/cosmos.ts
let _client: CosmosClient | null = null;
let _database: Database | null = null;
const _containers = new Map<string, Container>();

export async function getContainer(name: string = containerName): Promise<Container> {
  if (!_containers.has(name)) {
    const db = await getDatabase();
    const { container } = await db.containers.createIfNotExists({
      id: name,
      partitionKey: { paths: ['/id'] },
    });
    _containers.set(name, container);
  }
  return _containers.get(name)!;
}

We use a Map because we have three containers — items, blog and contact. getContainer() gives the default one. getContainer(blogContainerName) gives the blog. One function for all of them.

Also note the client is created on first use, not on import. If a request does not touch the database, it pays nothing.

Choose the Partition Key

Every container here uses /id as the partition key. So each document is its own partition.

This looks wrong at first. But for a blog it is the right choice.

  • The point read is the fast path. container.item(slug, slug).read() costs about 1 RU. That is the cheapest operation in Cosmos DB, and it is exactly what a blog page needs.
  • The data spreads evenly. There is no hot partition, ever.
  • We never need a transaction across documents. Cosmos DB gives you a batch inside one partition key only. We do not need it.

One more small idea. The slug is the id. There is no separate GUID and no lookup table from slug to id. So the URL itself is a point read.

If your app reads "all orders for customer X", then partition by customer, not by id. Follow your read pattern.

Query the List

The comments live in the same blog container as the posts. We tell them apart with a docType field. But the old posts imported from WordPress do not have this field at all.

SELECT c.id, c.slug, c.title, c.excerpt, c.pubDate, c.categories, c.tags, c.status
FROM c
WHERE (NOT IS_DEFINED(c.docType) OR c.docType = 'post')
  AND (NOT IS_DEFINED(c.status) OR c.status = 'publish')
ORDER BY c.pubDate DESC

NOT IS_DEFINED(...) OR ... is very useful in a NoSQL database. In SQL Server you would run an ALTER TABLE and update the old rows. Here you simply treat "field missing" as the old default. New documents always write the field. Old documents keep working and you change nothing.

Also see that we select the field names, not SELECT *. The contentMarkdown field is the biggest field in a post and the list page does not show it. In Cosmos DB the RU cost grows with the bytes you read, so this is a real saving on your busiest endpoint.

Hide the Draft Posts

The editor needs to save an unfinished post. The visitor must not see it. There are two endpoints and both need the rule.

For the list, we run the same query without the status filter when the caller is logged in. So the admin sees drafts in the same page, and the database returns only what the caller is allowed to see. No filtering in JavaScript.

For the single post, GET /api/blog/{slug} is a point read, so it cannot filter. We check after the read. If the post is a draft and the caller is not logged in, return 404, not 403.

This small difference matters. A 403 tells the visitor that the slug exists, so anyone can guess URLs and learn what you are writing. A 404 tells them nothing.

Fail Fast on Missing Settings

if (!connectionString) {
  throw new Error(
    'Missing required environment variable: COSMOS_CONNECTION_STRING. ' +
    'Set it in local.settings.json for local development, ' +
    'or in Azure Static Web Apps Application Settings for production.'
  );
}

This runs at module level, so a wrong deployment fails immediately instead of giving strange 500 errors later. The message names the variable and both places you may need to set it. The person reading this error at midnight should know what to do.

Conclusion

In this article we learned how to create a Cosmos DB account, connect it from Azure Functions with a single cached client, pick /id as the partition key for a content site, and hide draft posts with a 404.

Reference

#cosmos-db#azure-functions#nosql#typescript#partition-key

Comments

Be the first to comment.

Leave a comment

Never shown publicly.

Comments are reviewed before appearing publicly.