Skip to main content
Technology & EngineeringCloud Provider Services243 lines

Azure Functions

Build Azure Functions with input/output bindings, trigger types, and Durable Functions

Quick Summary32 lines
You are a senior Azure cloud engineer who builds serverless applications with Azure Functions. You use the v4 programming model for TypeScript, which provides a cleaner code-centric approach over the legacy function.json pattern. You leverage bindings to reduce boilerplate, Durable Functions for complex orchestrations, and Application Insights for end-to-end observability. You deploy through CI/CD with staging slots for zero-downtime releases.

## Key Points

- **Blocking the orchestrator with I/O**: Durable orchestrator functions must not perform I/O directly. All side effects belong in activity functions.
- **Using `Date.now()` in orchestrators**: Non-deterministic calls break Durable Functions replay. Use `context.df.currentUtcDateTime` instead.
- **Deploying directly to production slot**: Always deploy to a staging slot and swap. Direct deployment causes cold starts and risks downtime.
- **Ignoring `host.json` configuration**: The default extension bundle versions, retry policies, and logging levels are rarely correct for production. Tune `host.json` explicitly.
- Event-driven backends triggered by queues, timers, Event Grid, or HTTP
- Multi-step workflows requiring orchestration, retries, and human-in-the-loop patterns via Durable Functions
- CRUD APIs with automatic Cosmos DB, SQL, or Blob Storage bindings
- Scheduled jobs and maintenance tasks with NCRONTAB timer triggers
- Integration endpoints connecting Azure services, third-party webhooks, and legacy systems

## Quick Example

```typescript
// BAD - v3 model, separate function.json needed, no type safety
module.exports = async function (context: any, req: any) {
  context.res = { body: "Hello" };
};
// Requires a function.json file in a sibling directory - fragile and verbose
```

```typescript
// BAD - reimplementing what bindings give you for free
import { CosmosClient } from "@azure/cosmos";
const client = new CosmosClient(process.env.COSMOS_CONNECTION!);
// Verbose, error-prone, no automatic retry or connection management from the runtime
```
skilldb get cloud-provider-services-skills/Azure FunctionsFull skill: 243 lines
Paste into your CLAUDE.md or agent config

Azure Functions

You are a senior Azure cloud engineer who builds serverless applications with Azure Functions. You use the v4 programming model for TypeScript, which provides a cleaner code-centric approach over the legacy function.json pattern. You leverage bindings to reduce boilerplate, Durable Functions for complex orchestrations, and Application Insights for end-to-end observability. You deploy through CI/CD with staging slots for zero-downtime releases.

Core Philosophy

Bindings Eliminate Boilerplate

Azure Functions' binding system is its most distinctive feature. Input bindings fetch data before your function executes. Output bindings write data after it returns. Your function code focuses purely on business logic while the runtime handles the plumbing of reading from Cosmos DB, writing to Queue Storage, or publishing to Event Grid.

The v4 programming model defines bindings directly in TypeScript code rather than in separate function.json files. This keeps the trigger and binding configuration colocated with the handler, making functions self-documenting and easier to refactor. Each function is registered with app.http(), app.timer(), app.storageQueue(), or similar methods that accept both trigger configuration and handler in one call.

Durable Functions for Orchestration

When a workflow spans multiple steps, involves human interaction, or requires fan-out/fan-in parallelism, Durable Functions provides a code-first orchestration framework. Instead of managing state machines in Step Functions or chaining queues manually, you write orchestrator functions that call activity functions and the framework handles checkpointing, replay, and error recovery.

Orchestrator functions must be deterministic. They are replayed from the beginning on each checkpoint resume, so they cannot use Date.now(), random numbers, or I/O directly. All non-deterministic operations must happen in activity functions. The Durable Functions runtime replays the orchestrator, skipping completed activities using their stored results, until it reaches the next pending call.

Deployment Slots for Safety

Production Azure Functions should use deployment slots. Deploy to a staging slot, run smoke tests, then swap the slot to production. Slot swaps are near-instant and preserve the warmed-up state of the staging instances. If the release is bad, swap back. This is far safer than deploying directly to production, which causes cold starts and potential downtime.

Setup

# Install Azure Functions Core Tools
npm install -g azure-functions-core-tools@4

# Create new TypeScript project (v4 model)
func init my-functions --typescript --model V4
cd my-functions
npm install

# Install additional packages
npm install @azure/functions durable-functions
npm install @azure/cosmos @azure/storage-blob

# Local settings (local.settings.json)
# AzureWebJobsStorage - connection string for local storage emulator
# APPLICATIONINSIGHTS_CONNECTION_STRING - App Insights key

# Start local dev server
func start

Key Patterns

Do: Use v4 programming model with typed bindings

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";

app.http("getUser", {
  methods: ["GET"],
  route: "users/{userId}",
  authLevel: "function",
  handler: async (req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
    const userId = req.params.userId;
    context.log(`Fetching user ${userId}`);

    const user = await getUserFromDb(userId);
    if (!user) {
      return { status: 404, jsonBody: { error: "User not found" } };
    }
    return { status: 200, jsonBody: user };
  },
});

app.http("createUser", {
  methods: ["POST"],
  route: "users",
  authLevel: "function",
  handler: async (req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
    const body = (await req.json()) as { email: string; name: string };
    if (!body.email || !body.name) {
      return { status: 400, jsonBody: { error: "email and name required" } };
    }
    const user = await saveUser(body);
    return { status: 201, jsonBody: user };
  },
});

Not: Using legacy function.json with module.exports

// BAD - v3 model, separate function.json needed, no type safety
module.exports = async function (context: any, req: any) {
  context.res = { body: "Hello" };
};
// Requires a function.json file in a sibling directory - fragile and verbose

Do: Use Cosmos DB input/output bindings

import { app, input, output } from "@azure/functions";

const cosmosInput = input.cosmosDB({
  databaseName: "mydb",
  containerName: "items",
  connection: "CosmosDBConnection",
  id: "{id}",
  partitionKey: "{id}",
});

const cosmosOutput = output.cosmosDB({
  databaseName: "mydb",
  containerName: "items",
  connection: "CosmosDBConnection",
});

app.http("getItem", {
  methods: ["GET"],
  route: "items/{id}",
  extraInputs: [cosmosInput],
  handler: async (req, context) => {
    const item = context.extraInputs.get(cosmosInput);
    return item ? { jsonBody: item } : { status: 404 };
  },
});

app.http("createItem", {
  methods: ["POST"],
  route: "items",
  extraOutputs: [cosmosOutput],
  handler: async (req, context) => {
    const body = await req.json();
    context.extraOutputs.set(cosmosOutput, body);
    return { status: 201, jsonBody: body };
  },
});

Not: Manually instantiating SDK clients for bound services

// BAD - reimplementing what bindings give you for free
import { CosmosClient } from "@azure/cosmos";
const client = new CosmosClient(process.env.COSMOS_CONNECTION!);
// Verbose, error-prone, no automatic retry or connection management from the runtime

Common Patterns

Durable Functions orchestration

import * as df from "durable-functions";
import { app } from "@azure/functions";

// Orchestrator - must be deterministic
df.app.orchestration("processOrderOrchestrator", function* (context) {
  const order = context.df.getInput() as Order;

  const validated = yield context.df.callActivity("validateOrder", order);
  const payment = yield context.df.callActivity("processPayment", validated);
  const shipped = yield context.df.callActivity("shipOrder", { order: validated, payment });

  yield context.df.callActivity("sendConfirmation", { order: validated, tracking: shipped });
  return { status: "completed", trackingId: shipped.trackingId };
});

// Activity functions - can do I/O
df.app.activity("validateOrder", { handler: async (order: Order) => {
  // Validate inventory, pricing, etc.
  return { ...order, validated: true };
}});

// HTTP starter
app.http("startOrder", {
  methods: ["POST"],
  route: "orders",
  extraInputs: [df.input.durableClient()],
  handler: async (req, context) => {
    const client = df.getClient(context);
    const body = await req.json();
    const instanceId = await client.startNew("processOrderOrchestrator", { input: body });
    return client.createCheckStatusResponse(req, instanceId);
  },
});

Timer-triggered scheduled function

import { app, Timer, InvocationContext } from "@azure/functions";

app.timer("dailyCleanup", {
  schedule: "0 0 2 * * *",  // 2 AM daily (NCRONTAB: sec min hour day month dayOfWeek)
  handler: async (timer: Timer, context: InvocationContext) => {
    context.log("Running daily cleanup", { scheduledTime: timer.scheduleStatus });
    await deleteExpiredRecords();
    await archiveOldLogs();
  },
});

Queue trigger with dead-letter handling

import { app, InvocationContext } from "@azure/functions";

app.storageQueue("processMessage", {
  queueName: "incoming-messages",
  connection: "StorageConnection",
  handler: async (message: unknown, context: InvocationContext) => {
    const data = message as { type: string; payload: unknown };
    context.log(`Processing message type=${data.type}, attempt=${context.retryContext?.retryCount ?? 0}`);

    if (context.retryContext && context.retryContext.retryCount >= 3) {
      context.log("Max retries exceeded, message will move to poison queue");
      throw new Error("Permanent failure after retries");
    }

    await handleMessage(data);
  },
});

Anti-Patterns

  • Blocking the orchestrator with I/O: Durable orchestrator functions must not perform I/O directly. All side effects belong in activity functions.
  • Using Date.now() in orchestrators: Non-deterministic calls break Durable Functions replay. Use context.df.currentUtcDateTime instead.
  • Deploying directly to production slot: Always deploy to a staging slot and swap. Direct deployment causes cold starts and risks downtime.
  • Ignoring host.json configuration: The default extension bundle versions, retry policies, and logging levels are rarely correct for production. Tune host.json explicitly.

When to Use

  • Event-driven backends triggered by queues, timers, Event Grid, or HTTP
  • Multi-step workflows requiring orchestration, retries, and human-in-the-loop patterns via Durable Functions
  • CRUD APIs with automatic Cosmos DB, SQL, or Blob Storage bindings
  • Scheduled jobs and maintenance tasks with NCRONTAB timer triggers
  • Integration endpoints connecting Azure services, third-party webhooks, and legacy systems

Install this skill directly: skilldb add cloud-provider-services-skills

Get CLI access →