Run on Azure Functions

Run Understudy as one stateless Azure Function, with Azure Blob for state. The same file runs locally on Azurite and deployed in the cloud.

Understudy runs on Azure Functions as a single, stateless function. The function adapts each request onto Understudy and returns its response. A function keeps nothing on local disk between calls, so the data lives in Azure Blob Storage, and the same file runs on your machine and in the cloud.

One file is the whole host

A catch-all HTTP function passes every request to understudy.handle, which dispatches the admin API, the simulated API, and the editor UI. State goes to Blob, reusing the function app's own storage account, so there is nothing extra to configure.

// index.mjs
import { app } from '@azure/functions';
import { createUnderstudy } from '@xtrable-ltd/understudy/host';
import { BlobStore } from '@xtrable-ltd/understudy/adapter-azure-blob';

const understudy = createUnderstudy({
  store: new BlobStore({
    connectionString: process.env.AzureWebJobsStorage,
    containerName: 'understudy',
  }),
});

app.http('understudy', {
  route: '{*path}',
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  authLevel: 'anonymous',
  handler: async (request) => {
    const url = new url(request.url);
    const headers = {};
    request.headers.forEach((value, key) => { headers[key] = value; });

    const result = await understudy.handle({
      method: request.method,
      path: url.pathname,
      query: url.searchParams,
      headers,
      body: async () => new Uint8Array(await request.arrayBuffer()),
    });

    // Static assets are bytes; JSON responses are strings; a 204 (e.g. DELETE)
    // has no body. Only attach body when present (see the note below).
    const body = result.body instanceof Uint8Array
      ? Buffer.from(result.body)
      : result.body;

    return {
      status: result.status,
      headers: result.headers,
      ...(body === undefined ? {} : { body }),
    };
  },
});

One detail in that mapping matters. A 204 No Content response, which a successful DELETE returns, has no body, so the host only sets body when there is one. If you instead coerce an absent body to an empty string, Azure Functions rejects the whole response: the Response object it builds does not allow a body on a 204 (or a 205 or 304), and every delete fails with "Invalid response status code 204".

Two settings that matter

  • Empty route prefix. In host.json, set extensions.http.routePrefix to an empty string. Azure serves routes under /api by default, which would clash with Understudy's own /api admin routes.
  • Worker indexing. Set AzureWebJobsFeatureFlags to EnableWorkerIndexing, or the function registers nothing at startup and every request quietly returns 404.
// host.json
{
  "version": "2.0",
  "extensions": { "http": { "routePrefix": "" } }
}

Run it locally

Add a package.json with the two dependencies and a local.settings.json, then start the Functions emulator. It runs against Azurite, the local Blob emulator.

// package.json
{
  "type": "module",
  "main": "index.mjs",
  "dependencies": {
    "@azure/functions": "^4.5.0",
    "@xtrable-ltd/understudy": "^0.2.0"
  }
}
// local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true"
  }
}
npx azurite --skipApiVersionCheck   # in one terminal
npm install
func start                          # in another; opens on http://localhost:7071

Start Azurite with --skipApiVersionCheck. The Blob SDK asks for a newer REST version than current Azurite releases allow, and without the flag stateful calls fail while the editor UI still loads, which is a confusing mix. It is a local-only concern; real Azure Blob needs nothing.

Deploy it

Publish with the Azure Functions Core Tools (a user-level npm global, no admin rights):

func azure functionapp publish your-function-app

In the cloud you set nothing extra for Understudy. The function app already has an AzureWebJobsStorage connection string for its own storage account, and Understudy reuses it as the store. Locally that value points at Azurite; in the cloud it is the real account. The same index.mjs runs in both places, unchanged.