npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@apvee/azure-functions-openapi

v2.0.0

Published

An extension for Azure Functions V4 that provides support for exporting OpenAPI spec files from annotated Azure Functions.

Readme

@apvee/azure-functions-openapi

License: MIT

Version 2.0 - A complete rewrite with improved TypeScript support, automatic type inference, and enhanced Azure integration.

Apvee Azure Functions OpenAPI v2

📖 Overview

@apvee/azure-functions-openapi is a powerful extension for Azure Functions V4 that automatically generates and serves OpenAPI documentation for your serverless APIs. Built on top of @asteasolutions/zod-to-openapi and leveraging Zod schemas for validation, it ensures your API is always type-safe, well-documented, and easy to explore.

What Makes v2.0 Special?

This major release introduces a modern, ergonomic API through TypeScript module augmentation, directly extending the @azure/functions app object with intuitive methods. Write less boilerplate, get full type inference, and enjoy seamless integration with Azure Functions runtime.

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";
import { z } from "zod";

// Setup OpenAPI documentation
app.openAPISetup({
  info: { title: "My API", version: "1.0.0" },
});

// Register a fully typed endpoint
app.openAPIPath("GetUser", "Get user by ID", {
  typedHandler: async ({ params, context }) => {
    // params.id is automatically typed as string!
    const user = await getUser(params.id);
    return { jsonBody: user };
  },
  methods: ["GET"],
  route: "users/{id}",
  params: z.object({ id: z.string().uuid() }),
  response: UserSchema,
});

Key Capabilities

  • 🎯 Automatic Type Inference - TypeScript infers all parameter types from your Zod schemas
  • 📝 Multi-Version OpenAPI - OpenAPI 3.1.0 by default, with opt-in OpenAPI 3.0.3 and Swagger 2.0 output
  • 🎨 Integrated Swagger UI - Beautiful, interactive API documentation out of the box
  • 🔒 Azure Security Built-in - Native support for Function Keys, EasyAuth, and Azure AD
  • Runtime Validation - Automatic request/response validation with detailed error messages
  • 🌐 Webhook Support - Document callback endpoints with OpenAPI 3.1.0 webhooks
  • 🚀 Zero Config - Sensible defaults with full customization when needed

✨ What's New in v2.0

Version 2.0 is a complete rewrite that brings significant improvements in developer experience, performance, and Azure integration. Here's what changed:

🎨 Modern API with Module Augmentation

Before (v1.x):

import {
  registerFunction,
  registerOpenAPIHandler,
} from "@apvee/azure-functions-openapi";

registerOpenAPIHandler("anonymous", config, "3.1.0", "json");
registerFunction("GetUser", "Get user", {
  /* ... */
});

Now (v2.x):

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";

app.openAPISetup({ info: { title: 'My API', version: '1.0.0' } });
app.openAPIPath('GetUser', 'Get user', { /* ... */ });

The new API extends Azure Functions natively, providing better IDE support and a more intuitive developer experience.

🎯 Automatic Type Inference with Typed Handlers

The biggest feature in v2.0 is automatic type inference. No more manual type assertions!

app.openAPIPath('UpdateUser', 'Update user information', {
  typedHandler: async ({ params, body, query, context }) => {
    // All parameters are automatically typed from your schemas!
    // params.id: string (from UUID schema)
    // body.name: string
    // body.email: string
    // query.notify: boolean | undefined

    await updateUser(params.id, body);
    return { jsonBody: { success: true } };
  },
  methods: ["PUT"],
  route: "users/{id}",
  params: z.object({ id: z.string().uuid() }),
  body: z.object({
    name: z.string().min(1),
    email: z.string().email(),
  }),
  query: z.object({
    notify: z.boolean().optional(),
  }),
});

🔒 Enhanced Azure Security Support

v2.0 introduces native support for multiple Azure authentication methods:

  • Azure Function Keys - Built-in support for function, admin, and anonymous auth levels
  • Azure EasyAuth - Integration with App Service Authentication (AAD, Google, Facebook, GitHub, etc.)
  • Azure AD Bearer Token - Manual JWT validation for Microsoft Entra ID
  • Azure AD Client Credentials - Service-to-service authentication with app roles
  • Custom API Keys - Flexible header/query/cookie-based authentication
  • Safe host resolution - servers is never derived from the request Host header unless you explicitly opt in with trustHostHeader
// Azure EasyAuth example
const easyAuth = app.openAPIEasyAuth('aad');

app.openAPIPath('GetProfile', 'Get user profile', {
  handler: getProfileHandler,
  methods: ["GET"],
  route: "profile",
  authLevel: "anonymous", // Required for EasyAuth
  security: [easyAuth],
  response: ProfileSchema,
});

📦 Local Swagger UI Assets

v2.0 serves Swagger UI assets locally from node_modules instead of loading from CDN:

  • Better Performance - No external dependencies, faster load times
  • Offline Support - Works without internet connection
  • Security - No third-party CDN risks
  • Monorepo Support - Automatically detects local or root node_modules
  • Caching - Built-in ETag support for optimal caching

🔄 Zod 4 Compatibility

Fully aligned with Zod 4.x, ensuring compatibility with the latest validation features and improvements:

{
  "peerDependencies": {
    "@azure/functions": "^4.5.2",
    "zod": "^4.0.0"
  }
}

🌐 OpenAPI 3.1.0 Webhook Support

Document callback endpoints that your API calls using the new webhooks feature:

app.openAPIWebhook('OrderCreated', 'Notify when order is created', {
  typedHandler: async ({ body, context }) => {
    context.log(`Webhook received: Order ${body.orderId}`);
    return { jsonBody: { received: true } };
  },
  methods: ["POST"],
  body: OrderEventSchema,
  responses: [{ httpCode: 200, description: "Success" }],
});

⚡ Simplified Configuration

Setup is now much simpler with a single openAPISetup() call:

// Only OpenAPI 3.1.0 in JSON/YAML is emitted by default.
// Add 3.0.3 or 2.0 only when your tooling needs them.
app.openAPISetup({
  info: { title: 'My API', version: '1.0.0' },
  routePrefix: 'api',
  versions: ['3.1.0', '3.0.3', '2.0'], // Optional, defaults to ['3.1.0']
  formats: ['json', 'yaml'],           // Optional, defaults to ['json', 'yaml']
  servers: [{ url: 'https://api.example.com' }],
  trustHostHeader: false,               // Optional, defaults to false
  swaggerUI: { 
    enabled: true,                     // Optional, defaults to true
    route: 'docs'                      // Optional, defaults to 'swagger-ui'
  }
});

🛠️ New Utility Functions

Export powerful utilities for manual validation and parsing:

  • parseRouteParams() - Validate route parameters
  • parseQueryParams() - Validate query strings
  • parseBody() - Validate request body
  • parseHeaders() - Validate headers
  • parseEasyAuthPrincipal() - Decode Azure EasyAuth user info
  • extractFunctionKey() - Extract function keys from requests
  • createTypedHandler() - Create typed handlers programmatically

📋 Breaking Changes Summary

While v2.0 brings many improvements, some APIs have changed. See the Migration Guide below for detailed migration steps.

Removed APIs:

  • registerOpenAPIHandler() → Use app.openAPISetup()
  • registerSwaggerUIHandler() → Use app.openAPISetup()
  • registerFunction() → Use app.openAPIPath() or app.openAPIWebhook()
  • registerApiKeySecuritySchema() → Use app.openAPIKeySecurity()
  • registerTypeSchema() → Use app.openAPISchema()

Private APIs:

  • convertHttpRequestParamsToObject() - Now internal, use parseRouteParams() instead
  • convertURLSearchParamsToObject() - Now internal, use parseQueryParams() instead

🎯 Why Document APIs with OpenAPI?

OpenAPI documentation is more than just nice-to-have—it's a fundamental part of modern API development that delivers tangible benefits to both API producers and consumers.

Always Up-to-Date Documentation

By integrating OpenAPI documentation directly into your codebase, you ensure that your API documentation is always in sync with the implementation. No more outdated docs, no more discrepancies between what's documented and what's actually deployed.

// Documentation is generated from the same schemas used for validation
app.openAPIPath('CreateUser', 'Create a new user', {
  typedHandler: async ({ body, context }) => {
    // This schema validates the request AND generates the documentation
    return { status: 201, jsonBody: await createUser(body) };
  },
  methods: ["POST"],
  route: "users",
  body: z.object({
    name: z.string().min(1).describe("User full name"),
    email: z.string().email().describe("Valid email address"),
    age: z.number().int().positive().optional().describe("User age"),
  }),
  response: UserSchema,
});

Auto-Generated Client Libraries

OpenAPI specifications can be used to automatically generate type-safe client libraries for various programming languages:

This saves development time and ensures API consumers have reliable, type-safe SDKs without manual coding.

Enhanced Developer Experience

Comprehensive and accurate API documentation makes it easier for developers to understand and use your API:

  • Interactive Testing - Swagger UI allows developers to test endpoints directly from documentation
  • Type Definitions - Clear request/response schemas with examples
  • Authentication Details - Security requirements clearly documented
  • Validation Rules - Understand constraints before making requests

Improved API Testing

OpenAPI documentation enables:

  • Contract Testing - Validate that implementation matches the spec
  • Mock Servers - Generate mock APIs for frontend development
  • Test Case Generation - Automatically create test scenarios from schemas
  • Response Validation - Ensure API responses match documented structure

Standardization and Interoperability

OpenAPI is a widely adopted industry standard supported by thousands of tools and platforms:

  • API Gateways (Azure API Management, Kong, AWS API Gateway)
  • Monitoring Tools (Postman, Insomnia, Paw)
  • Testing Frameworks (Dredd, Schemathesis, REST Assured)
  • Documentation Platforms (Redoc, Stoplight, Readme.io)

By documenting with OpenAPI, your API becomes easily integrable with the entire ecosystem.


🚀 Key Features

Module Augmentation API

Extends @azure/functions natively with intuitive OpenAPI methods. No separate imports needed—just extend the app object you already use.

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";

// All app.openAPIXxx() methods are now available
app.openAPISetup({ /* config */ });
app.openAPIPath('MyEndpoint', 'Description', { /* config */ });

Automatic Type Inference

TypeScript automatically infers parameter types from Zod schemas. No manual type assertions, no type mismatches.

app.openAPIPath('UpdateTodo', 'Update a todo item', {
  typedHandler: async ({ params, body }) => {
    // params.id is automatically string (from UUID schema)
    // body.title is automatically string
    // body.completed is automatically boolean
    const updated = { id: params.id, ...body };
    return { jsonBody: updated };
  },
  methods: ["PUT"],
  route: "todos/{id}",
  params: z.object({ id: z.string().uuid() }),
  body: z.object({
    title: z.string(),
    completed: z.boolean(),
  }),
});

Multi-Version OpenAPI Support

Generate OpenAPI specifications in multiple versions and formats when your tooling needs them. By default, the library emits OpenAPI 3.1.0 in JSON and YAML only.

  • OpenAPI 3.1.0 - Latest spec with full JSON Schema support and webhooks
  • OpenAPI 3.0.3 - Widely supported by most tools
  • OpenAPI 2.0 (Swagger) - Legacy support for older tools

Add versions: ["3.1.0", "3.0.3", "2.0"] to generate all supported versions simultaneously. Export in JSON or YAML format to suit your needs.

Integrated Swagger UI

Beautiful, interactive API documentation served automatically:

app.openAPISetup({
  info: { title: 'My API', version: '1.0.0' },
  swaggerUI: {
    enabled: true,
    route: "docs", // Access at: http://localhost:7071/docs
  },
});

Swagger UI assets are served locally for better performance, security, and offline support.

Type-Safe Request Validation

Leverage Zod schemas to validate all aspects of HTTP requests:

  • Route Parameters - /users/{id} with UUID validation
  • Query Strings - Pagination, filtering, sorting with type coercion
  • Request Body - JSON payloads with nested object validation
  • Headers - Custom headers like API keys or tokens

Validation errors automatically return 400 Bad Request with detailed error messages.

Comprehensive Azure Security Support

Native integration with Azure authentication mechanisms:

| Security Method | Use Case | Example | | ---------------------- | --------------------------- | ----------------------------- | | Function Keys | Azure native key-based auth | API keys managed by Azure | | EasyAuth | App Service Authentication | AAD, Google, Facebook, GitHub | | Azure AD Bearer | Manual JWT validation | Entra ID token validation | | Client Credentials | Service-to-service auth | Daemon apps, background jobs | | Custom API Keys | User-implemented auth | Header/query/cookie keys |

// Example: Azure AD Bearer Token
const adAuth = app.openAPIAzureADBearer({
  name: 'AzureAD',
  scopes: ['User.Read', 'Mail.Send']
});

app.openAPIPath('SendEmail', 'Send email on behalf of user', {
  handler: sendEmailHandler,
  methods: ["POST"],
  route: "send-email",
  security: [adAuth],
  body: EmailSchema,
});

OpenAPI 3.1.0 Webhooks

Document callback endpoints that your API calls using the webhooks feature:

app.openAPIWebhook('PaymentCompleted', 'Notify when payment is completed', {
  typedHandler: async ({ body, context }) => {
    context.log(`Payment ${body.paymentId} completed`);
    return { jsonBody: { acknowledged: true } };
  },
  methods: ["POST"],
  body: PaymentEventSchema,
});

Advanced Response Configuration

Support for complex response scenarios:

  • Multiple Status Codes - Document 200, 201, 400, 404, 500, etc.
  • Multiple Content Types - JSON, XML, PDF, CSV in the same endpoint
  • Response Headers - Rate limits, pagination info, custom headers
  • Detailed Descriptions - Clear documentation for each response
app.openAPIPath('GetReport', 'Get report in multiple formats', {
  handler: getReportHandler,
  methods: ["GET"],
  route: "reports/{id}",
  params: z.object({ id: z.string() }),
  responses: [
    {
      httpCode: 200,
      description: "Report retrieved successfully",
      content: [
        { mediaType: "application/json", schema: JsonReportSchema },
        { mediaType: "application/pdf", schema: z.instanceof(Buffer) },
        { mediaType: "text/csv", schema: z.string() },
      ],
    },
    {
      httpCode: 404,
      description: "Report not found",
      schema: ErrorSchema,
    },
  ],
});

Rich Utility Functions

Export powerful utilities for advanced use cases:

import {
  parseRouteParams,
  parseQueryParams,
  parseBody,
  parseHeaders,
  parseEasyAuthPrincipal,
  extractFunctionKey,
  createTypedHandler,
  ValidationError,
} from "@apvee/azure-functions-openapi";

Zero Configuration Defaults

Get started quickly with sensible defaults, customize when needed:

// Minimal setup - generates OpenAPI 3.1.0 in JSON/YAML + Swagger UI
app.openAPISetup({
  info: { title: 'My API', version: '1.0.0' }
});

// Or fully customize everything
app.openAPISetup({
  info: { /* ... */ },
  routePrefix: 'api',
  versions: ['3.1.0', '3.0.3', '2.0'],
  formats: ['json', 'yaml'],
  authLevel: 'anonymous',
  security: [globalSecurityScheme],
  servers: [{ url: "https://api.example.com" }],
  tags: [{ name: "Users", description: "User management" }],
  swaggerUI: {
    enabled: true,
    route: "docs",
    authLevel: "anonymous",
  },
});

📦 Installation

Install the package using npm, yarn, or pnpm:

npm install @apvee/azure-functions-openapi

Peer Dependencies

This library requires the following peer dependencies:

npm install @azure/functions zod

Version Compatibility

| Package | Version | Notes | | -------------------------------- | ---------- | ------------------------ | | @apvee/azure-functions-openapi | ^2.0.0 | This library | | @azure/functions | ^4.5.2 | Azure Functions runtime | | zod | ^4.0.0 | Schema validation | | Node.js | >=18.0.0 | Recommended: Node 20 LTS |

Complete Installation

For a new Azure Functions project with TypeScript:

# Create a new Azure Functions project
func init my-api --typescript

# Navigate to project
cd my-api

# Install dependencies
npm install @azure/functions zod
npm install @apvee/azure-functions-openapi

# Install dev dependencies
npm install -D typescript @types/node

Verification

Verify the installation by importing the package:

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";
import { z } from "zod";

console.log("✅ Installation successful!");

🚀 Quick Start

Get your first OpenAPI-documented Azure Function running in minutes.

Step 1: Setup OpenAPI

Create or edit your src/index.ts file:

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";

// Configure OpenAPI documentation
app.openAPISetup({
  info: {
    title: "My First API",
    version: "1.0.0",
    description: "A simple API with OpenAPI documentation",
  },
});

Step 2: Create Your First Endpoint

Add a simple GET endpoint:

import { z } from "zod";

// Define response schema
const GreetingSchema = z.object({
  message: z.string(),
  timestamp: z.string(),
});

// Register endpoint with OpenAPI documentation
app.openAPIPath('GetGreeting', 'Get a greeting message', {
  typedHandler: async ({ query, context }) => {
    const name = query.name || "World";

    context.log(`Greeting requested for: ${name}`);

    return {
      jsonBody: {
        message: `Hello, ${name}!`,
        timestamp: new Date().toISOString(),
      },
    };
  },
  methods: ["GET"],
  route: "greet",
  query: z.object({
    name: z.string().optional().describe("Name to greet"),
  }),
  response: GreetingSchema,
});

Step 3: Run Your Function

Start the Azure Functions runtime:

npm start
# or
func start

Step 4: Access Your API Documentation

Open your browser and navigate to:

  • Swagger UI: http://localhost:7071/swagger-ui
  • OpenAPI JSON: http://localhost:7071/api/openapi/3.1.0.json
  • OpenAPI YAML: http://localhost:7071/api/openapi/3.1.0.yaml

Step 5: Test Your Endpoint

Try your new endpoint:

# Without name parameter
curl http://localhost:7071/api/greet

# With name parameter
curl "http://localhost:7071/api/greet?name=Azure"

Response:

{
  "message": "Hello, Azure!",
  "timestamp": "2025-11-06T12:34:56.789Z"
}

Complete Quick Start Example

Here's the full src/index.ts file:

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";
import { z } from "zod";

// Setup OpenAPI
app.openAPISetup({
  info: {
    title: "My First API",
    version: "1.0.0",
    description: "A simple API with OpenAPI documentation",
    contact: {
      name: "API Support",
      email: "[email protected]",
    },
  },
  tags: [{ name: "Greetings", description: "Greeting endpoints" }],
});

// Define schemas
const GreetingSchema = z.object({
  message: z.string(),
  timestamp: z.string(),
});

// Register endpoint
app.openAPIPath('GetGreeting', 'Get a personalized greeting', {
  typedHandler: async ({ query, context }) => {
    const name = query.name || "World";
    context.log(`Greeting requested for: ${name}`);

    return {
      jsonBody: {
        message: `Hello, ${name}!`,
        timestamp: new Date().toISOString(),
      },
    };
  },
  methods: ["GET"],
  route: "greet",
  tags: ["Greetings"],
  query: z.object({
    name: z.string().optional().describe("Name to greet (default: World)"),
  }),
  response: GreetingSchema,
});

🎉 Congratulations! You now have a fully documented API with automatic type inference, request validation, and interactive Swagger UI.


📘 Core Concepts

Setup OpenAPI Documentation

The app.openAPISetup() method is the entry point for configuring OpenAPI documentation. Call it once in your src/index.ts file before registering any endpoints.

Basic Setup

Minimal configuration with defaults:

import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";

app.openAPISetup({
  info: {
    title: "My API",
    version: "1.0.0",
  },
});

This generates:

  • OpenAPI 3.1.0 in JSON and YAML formats
  • Swagger UI at /swagger-ui
  • Documents accessible at /api/openapi/3.1.0.json and /api/openapi/3.1.0.yaml

Complete Configuration

Full example with all available options:

app.openAPISetup({
  // Required: API metadata
  info: {
    title: "Todo API",
    version: "2.0.0",
    description: "A comprehensive todo management API",
    termsOfService: "https://example.com/terms",
    contact: {
      name: "API Support Team",
      email: "[email protected]",
      url: "https://example.com/support",
    },
    license: {
      name: "MIT",
      url: "https://opensource.org/licenses/MIT",
    },
  },

  // Optional: Server configurations
  // Recommended in production. When omitted, `servers` is not derived from
  // the request Host header unless `trustHostHeader` is explicitly enabled.
  servers: [
    {
      url: "https://api.example.com",
      description: "Production server",
    },
    {
      url: "https://staging-api.example.com",
      description: "Staging server",
    },
    {
      url: "http://localhost:7071",
      description: "Local development",
    },
  ],

  // Optional: Global security requirements
  security: [
    { BearerAuth: [] }, // Apply to all endpoints by default
  ],

  // Optional: External documentation
  externalDocs: {
    description: "Full API Documentation",
    url: "https://docs.example.com/api",
  },

  // Optional: Tags for organizing endpoints
  tags: [
    {
      name: "Todos",
      description: "Todo item operations",
      externalDocs: {
        description: "Todo guide",
        url: "https://docs.example.com/todos",
      },
    },
    {
      name: "Users",
      description: "User management operations",
    },
  ],

  // Optional: Azure Functions route prefix (default: 'api')
  routePrefix: "api",

  // Optional: Authorization level for OpenAPI endpoints (default: 'anonymous')
  authLevel: "anonymous",

  // Optional: OpenAPI versions to generate (default: ['3.1.0'])
  versions: ["3.1.0", "3.0.3", "2.0"],

  // Optional: Output formats (default: ['json', 'yaml'])
  formats: ["json", "yaml"],

  // Optional: trust the incoming Host header for generated `servers`
  // (default: false). Prefer explicit `servers` in production.
  trustHostHeader: false,

  // Optional: host allowlist used only when `trustHostHeader: true`
  trustedHosts: ["api.example.com", "staging-api.example.com"],

  // Optional: Swagger UI configuration
  swaggerUI: {
    enabled: true, // default: true
    route: "docs", // default: 'swagger-ui'
    authLevel: "anonymous", // default: same as main authLevel
  },
});

Configuration Options Reference

| Option | Type | Default | Description | | --------------------- | -------------------------------------- | ------------------------ | ------------------------------------------------ | | info | InfoObject | Required | API metadata (title, version, description, etc.) | | servers | ServerObject[] | undefined | Server URLs for different environments | | security | SecurityRequirementObject[] | undefined | Global security requirements | | externalDocs | ExternalDocumentationObject | undefined | Link to external documentation | | tags | TagObject[] | undefined | Tags for organizing endpoints | | routePrefix | string | 'api' | Azure Functions route prefix | | authLevel | 'anonymous' \| 'function' \| 'admin' | 'anonymous' | Auth level for OpenAPI/Swagger endpoints | | versions | Array<'2.0' \| '3.0.3' \| '3.1.0'> | ['3.1.0'] | OpenAPI versions to generate | | formats | Array<'json' \| 'yaml'> | ['json', 'yaml'] | Output formats | | trustHostHeader | boolean | false | Derive servers from the request Host header only when explicitly enabled | | trustedHosts | string[] | [] | Hostname allowlist used when trustHostHeader is enabled | | swaggerUI.enabled | boolean | true | Enable/disable Swagger UI | | swaggerUI.route | string | 'swagger-ui' | Swagger UI route | | swaggerUI.authLevel | 'anonymous' \| 'function' \| 'admin' | Same as main authLevel | Auth level for Swagger UI |

Generated Endpoints

Based on your configuration, the following endpoints are automatically created. With the default versions: ["3.1.0"], only the 3.1.0 document routes are registered; 3.0.3 and 2.0 routes are registered only when you add those versions.

OpenAPI Documents:

GET /{routePrefix}/openapi/3.1.0.json
GET /{routePrefix}/openapi/3.1.0.yaml
GET /{routePrefix}/openapi/3.0.3.json
GET /{routePrefix}/openapi/3.0.3.yaml
GET /{routePrefix}/openapi/2.0.json
GET /{routePrefix}/openapi/2.0.yaml

Swagger UI:

GET /{swaggerUI.route}
GET /{swaggerUI.route}/assets/{file}

Example: Multiple Environments

Configure different servers for different deployment stages:

app.openAPISetup({
  info: {
    title: "Multi-Environment API",
    version: "1.0.0",
  },
  servers: [
    {
      url: "https://{environment}.api.example.com",
      description: "Environment-based server",
      variables: {
        environment: {
          default: "prod",
          enum: ["prod", "staging", "dev"],
          description: "Environment name",
        },
      },
    },
  ],
});

Example: Secured Documentation

Require authentication to view OpenAPI docs and Swagger UI:

app.openAPISetup({
  info: {
    title: "Secured API",
    version: "1.0.0",
  },
  authLevel: "function", // Require function key
  swaggerUI: {
    enabled: true,
    route: "docs",
    authLevel: "admin", // Require admin key for Swagger UI
  },
});

Access requires a key:

# Access OpenAPI with function key
curl "http://localhost:7071/api/openapi/3.1.0.json?code=YOUR_FUNCTION_KEY"

# Access Swagger UI with admin key
curl "http://localhost:7071/docs?code=YOUR_ADMIN_KEY"

Registering HTTP Endpoints

The app.openAPIPath() method registers HTTP endpoints with OpenAPI documentation. It replaces the traditional app.http() method and automatically handles both Azure Functions registration and OpenAPI documentation generation.

Basic Endpoint

Simple GET endpoint without parameters:

import { z } from "zod";

const StatusSchema = z.object({
  status: z.string(),
  timestamp: z.string(),
  version: z.string(),
});

app.openAPIPath('GetStatus', 'Get API status', {
  handler: async (request, context) => {
    return {
      jsonBody: {
        status: "healthy",
        timestamp: new Date().toISOString(),
        version: "1.0.0",
      },
    };
  },
  methods: ["GET"],
  route: "status",
  response: StatusSchema,
});

Endpoint with Path Parameters

Extract values from the URL path:

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
});

app.openAPIPath('GetUser', 'Get user by ID', {
  typedHandler: async ({ params, context }) => {
    // params.id is automatically typed as string and validated as UUID
    context.log(`Fetching user: ${params.id}`);

    const user = await getUserById(params.id);
    return { jsonBody: user };
  },
  methods: ["GET"],
  route: "users/{id}",
  params: z.object({
    id: z.string().uuid().describe("User unique identifier"),
  }),
  response: UserSchema,
});

Endpoint with Query Parameters

Handle query strings with validation:

const TodoListSchema = z.object({
  todos: z.array(TodoSchema),
  total: z.number(),
  page: z.number(),
  pageSize: z.number(),
});

app.openAPIPath('ListTodos', 'List todos with pagination', {
  typedHandler: async ({ query, context }) => {
    // query parameters are automatically typed and coerced
    const page = query.page || 1;
    const pageSize = query.pageSize || 10;
    const status = query.status;

    const todos = await getTodos({ page, pageSize, status });

    return {
      jsonBody: {
        todos,
        total: todos.length,
        page,
        pageSize,
      },
    };
  },
  methods: ["GET"],
  route: "todos",
  query: z.object({
    page: z.coerce.number().int().positive().default(1).describe("Page number"),
    pageSize: z.coerce
      .number()
      .int()
      .positive()
      .max(100)
      .default(10)
      .describe("Items per page"),
    status: z
      .enum(["pending", "completed", "all"])
      .optional()
      .describe("Filter by status"),
  }),
  response: TodoListSchema,
});

Endpoint with Request Body

POST/PUT/PATCH endpoints with JSON body:

const CreateTodoSchema = z.object({
  title: z.string().min(1).max(200).describe("Todo title"),
  description: z.string().optional().describe("Todo description"),
  dueDate: z.string().datetime().optional().describe("Due date in ISO format"),
});

const TodoSchema = z.object({
  id: z.string().uuid(),
  title: z.string(),
  description: z.string().optional(),
  dueDate: z.string().optional(),
  completed: z.boolean(),
  createdAt: z.string(),
});

app.openAPIPath('CreateTodo', 'Create a new todo', {
  typedHandler: async ({ body, context }) => {
    // body is automatically typed and validated
    context.log(`Creating todo: ${body.title}`);

    const newTodo = await createTodo(body);

    return {
      status: 201,
      jsonBody: newTodo,
    };
  },
  methods: ["POST"],
  route: "todos",
  body: CreateTodoSchema,
  response: TodoSchema,
});

Endpoint with Multiple HTTP Methods

Support multiple methods on the same route:

app.openAPIPath('ManageTodo', 'Manage todo item', {
  typedHandler: async ({ request, params, body, context }) => {
    const method = request.method;
    const todoId = params.id;

    switch (method) {
      case "GET":
        return { jsonBody: await getTodo(todoId) };
      case "PUT":
        return { jsonBody: await updateTodo(todoId, body) };
      case "DELETE":
        await deleteTodo(todoId);
        return { status: 204 };
      default:
        return { status: 405, body: "Method not allowed" };
    }
  },
  methods: ["GET", "PUT", "DELETE"],
  route: "todos/{id}",
  params: z.object({ id: z.string().uuid() }),
  body: UpdateTodoSchema,
  responses: [
    { httpCode: 200, schema: TodoSchema, description: "Todo retrieved" },
    { httpCode: 204, description: "Todo deleted" },
  ],
});

Endpoint with Custom Headers

Validate custom request headers:

app.openAPIPath('SecureEndpoint', 'Endpoint with custom API key', {
  typedHandler: async ({ headers, context }) => {
    // headers are automatically typed and validated
    const apiKey = headers["x-api-key"];

    context.log(`Request with API key: ${apiKey.substring(0, 8)}...`);

    return { jsonBody: { authenticated: true } };
  },
  methods: ["GET"],
  route: "secure/data",
  headers: z.object({
    "x-api-key": z.string().min(32).describe("API key for authentication"),
    "x-request-id": z
      .string()
      .uuid()
      .optional()
      .describe("Optional request tracking ID"),
  }),
  response: z.object({ authenticated: z.boolean() }),
});

Endpoint with Multiple Responses

Document different response codes:

const ErrorSchema = z.object({
  error: z.string(),
  code: z.string(),
  details: z.any().optional(),
});

app.openAPIPath('UpdateTodo', 'Update an existing todo', {
  typedHandler: async ({ params, body, context }) => {
    try {
      const todo = await getTodo(params.id);

      if (!todo) {
        return {
          status: 404,
          jsonBody: {
            error: "Todo not found",
            code: "TODO_NOT_FOUND",
          },
        };
      }

      const updated = await updateTodo(params.id, body);

      return {
        status: 200,
        jsonBody: updated,
      };
    } catch (error) {
      return {
        status: 500,
        jsonBody: {
          error: "Internal server error",
          code: "INTERNAL_ERROR",
          details: error.message,
        },
      };
    }
  },
  methods: ["PUT"],
  route: "todos/{id}",
  params: z.object({ id: z.string().uuid() }),
  body: UpdateTodoSchema,
  responses: [
    {
      httpCode: 200,
      description: "Todo updated successfully",
      schema: TodoSchema,
    },
    {
      httpCode: 404,
      description: "Todo not found",
      schema: ErrorSchema,
    },
    {
      httpCode: 500,
      description: "Internal server error",
      schema: ErrorSchema,
    },
  ],
});

Configuration Options

| Option | Type | Required | Description | | -------------- | -------------------------------------- | ---------------------------------- | ----------------------------------------- | | handler | HttpHandler | One of handler or typedHandler | Traditional Azure Functions handler | | typedHandler | TypedHandler | One of handler or typedHandler | Typed handler with auto validation | | methods | HttpMethod[] | ✅ Yes | HTTP methods (GET, POST, PUT, etc.) | | route | string | ✅ Yes | Route path (without prefix) | | params | ZodSchema | No | Route parameters schema | | query | ZodSchema | No | Query string parameters schema | | body | ZodSchema | No | Request body schema (JSON) | | headers | ZodObject | No | Request headers schema | | response | ZodSchema | No | Single response shortcut (assumes 200 OK) | | responses | ResponseConfig[] | No | Multiple response configurations | | authLevel | 'anonymous' \| 'function' \| 'admin' | No | Azure Functions auth level | | security | SecurityRequirementObject[] | No | Security requirements for this endpoint | | tags | string[] | No | OpenAPI tags for organization | | description | string | No | Detailed description | | deprecated | boolean | No | Mark endpoint as deprecated | | operationId | string | No | Unique operation ID (defaults to name) |

Traditional Handler vs Typed Handler

Traditional Handler - Manual parsing and validation:

app.openAPIPath('CreateUser', 'Create user', {
  handler: async (request, context) => {
    // Manual parsing required
    const body = await request.json();
    const { name, email } = body;

    // Manual validation
    if (!name || !email) {
      return { status: 400, body: "Missing required fields" };
    }

    const user = await createUser({ name, email });
    return { jsonBody: user };
  },
  methods: ["POST"],
  route: "users",
  body: CreateUserSchema,
});

Typed Handler - Automatic parsing and validation:

app.openAPIPath('CreateUser', 'Create user', {
  typedHandler: async ({ body, context }) => {
    // body is already parsed, validated, and typed!
    // No manual checks needed
    const user = await createUser(body);
    return { jsonBody: user };
  },
  methods: ["POST"],
  route: "users",
  body: CreateUserSchema,
});

Typed Handlers

The Typed Handler feature is the most powerful addition in v2.0. It provides automatic type inference from Zod schemas, eliminating manual type assertions and reducing boilerplate code.

How It Works

When you define schemas for params, query, body, or headers, TypeScript automatically infers the types for your handler parameters:

// Define schemas
const ParamsSchema = z.object({ id: z.string().uuid() });
const QuerySchema = z.object({ include: z.string().optional() });
const BodySchema = z.object({ title: z.string(), completed: z.boolean() });

app.openAPIPath('UpdateTodo', 'Update todo', {
  typedHandler: async ({ params, query, body, context }) => {
    // TypeScript knows:
    // - params.id is string (from UUID schema)
    // - query.include is string | undefined
    // - body.title is string
    // - body.completed is boolean

    // No type assertions needed! ✨
    const todo = await updateTodo(params.id, body);
    return { jsonBody: todo };
  },
  methods: ["PUT"],
  route: "todos/{id}",
  params: ParamsSchema,
  query: QuerySchema,
  body: BodySchema,
});

Automatic Validation

Typed handlers automatically validate all request data. If validation fails, a 400 Bad Request response is returned with detailed error information:

app.openAPIPath('CreateUser', 'Create new user', {
  typedHandler: async ({ body, context }) => {
    // If we reach here, body is guaranteed to be valid
    const user = await createUser(body);
    return { status: 201, jsonBody: user };
  },
  methods: ["POST"],
  route: "users",
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    age: z.number().int().positive().min(18),
  }),
});

Invalid request:

curl -X POST http://localhost:7071/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"","email":"invalid","age":15}'

Automatic error response:

{
  "error": "Request body validation failed",
  "issues": [
    {
      "path": ["name"],
      "message": "String must contain at least 1 character(s)"
    },
    {
      "path": ["email"],
      "message": "Invalid email"
    },
    {
      "path": ["age"],
      "message": "Number must be greater than or equal to 18"
    }
  ]
}

Handler Arguments

The typed handler receives a single object with the following properties:

type TypedHandlerArgs = {
  params: /* inferred from params schema */;
  query: /* inferred from query schema or URLSearchParams */;
  body: /* inferred from body schema or undefined */;
  headers: /* inferred from headers schema or raw headers */;
  request: SafeHttpRequest; // Original request (body methods removed if parsed)
  context: InvocationContext; // Azure Functions context
};

Example with all parameters:

app.openAPIPath('ComplexEndpoint', 'Example with all parameters', {
  typedHandler: async ({ params, query, body, headers, request, context }) => {
    context.log(`Method: ${request.method}`);
    context.log(`URL: ${request.url}`);
    context.log(`Params:`, params);
    context.log(`Query:`, query);
    context.log(`Body:`, body);
    context.log(`Headers:`, headers);

    return { jsonBody: { success: true } };
  },
  methods: ["POST"],
  route: "complex/{id}",
  params: z.object({ id: z.string() }),
  query: z.object({ filter: z.string().optional() }),
  body: z.object({ data: z.any() }),
  headers: z.object({
    "x-api-key": z.string(),
  }),
});

Type Inference Examples

Simple types:

// String parameter
params: z.object({ id: z.string() });
// → params.id is string

// Number with coercion
query: z.object({ page: z.coerce.number() });
// → query.page is number

// Boolean
body: z.object({ enabled: z.boolean() });
// → body.enabled is boolean

Optional types:

query: z.object({
  search: z.string().optional(),
  limit: z.coerce.number().default(10),
});
// → query.search is string | undefined
// → query.limit is number

Complex types:

body: z.object({
  user: z.object({
    name: z.string(),
    email: z.string().email(),
  }),
  tags: z.array(z.string()),
  metadata: z.record(z.string(), z.any()),
});
// → body.user is { name: string; email: string }
// → body.tags is string[]
// → body.metadata is Record<string, any>

Enums:

query: z.object({
  status: z.enum(["active", "inactive", "pending"]),
});
// → query.status is "active" | "inactive" | "pending"

Union types:

body: z.object({
  value: z.union([z.string(), z.number()]),
});
// → body.value is string | number

Using createTypedHandler

For reusable handlers, use the createTypedHandler utility:

import { createTypedHandler } from "@apvee/azure-functions-openapi";

// Define schemas
const TodoParamsSchema = z.object({ id: z.string().uuid() });
const UpdateTodoBodySchema = z.object({
  title: z.string().optional(),
  completed: z.boolean().optional(),
});

// Create reusable typed handler
const updateTodoHandler = createTypedHandler(
  {
    params: TodoParamsSchema,
    body: UpdateTodoBodySchema,
  },
  async ({ params, body, context }) => {
    // Fully typed parameters
    context.log(`Updating todo ${params.id}`);
    const updated = await updateTodo(params.id, body);
    return { jsonBody: updated };
  }
);

// Use in endpoint registration
app.openAPIPath('UpdateTodo', 'Update todo', {
  handler: updateTodoHandler, // Use as regular handler
  methods: ["PATCH"],
  route: "todos/{id}",
  params: TodoParamsSchema,
  body: UpdateTodoBodySchema,
  response: TodoSchema,
});

Inline vs External Handlers

Inline Handler (best for simple logic):

app.openAPIPath('DeleteTodo', 'Delete todo', {
  typedHandler: async ({ params, context }) => {
    await deleteTodo(params.id);
    return { status: 204 };
  },
  methods: ["DELETE"],
  route: "todos/{id}",
  params: z.object({ id: z.string().uuid() }),
});

External Handler (best for complex logic):

// In handlers/deleteTodo.ts
export const deleteTodoHandler = createTypedHandler(
  { params: TodoParamsSchema },
  async ({ params, context }) => {
    context.log(`Deleting todo: ${params.id}`);

    const todo = await getTodo(params.id);
    if (!todo) {
      return { status: 404, jsonBody: { error: "Todo not found" } };
    }

    await deleteTodo(params.id);
    return { status: 204 };
  }
);

// In index.ts
import { deleteTodoHandler } from "./handlers/deleteTodo";

app.openAPIPath('DeleteTodo', 'Delete todo', {
  handler: deleteTodoHandler,
  methods: ["DELETE"],
  route: "todos/{id}",
  params: TodoParamsSchema,
});

Error Handling in Typed Handlers

Validation errors are handled automatically, but you can add custom error handling:

app.openAPIPath('RiskyOperation', 'Operation that might fail', {
  typedHandler: async ({ body, context }) => {
    try {
      const result = await performRiskyOperation(body);
      return { jsonBody: result };
    } catch (error) {
      context.error("Operation failed:", error);

      if (error instanceof DatabaseError) {
        return {
          status: 503,
          jsonBody: { error: "Service temporarily unavailable" },
        };
      }

      return {
        status: 500,
        jsonBody: { error: "Internal server error" },
      };
    }
  },
  methods: ["POST"],
  route: "risky",
  body: OperationSchema,
  responses: [
    { httpCode: 200, schema: ResultSchema },
    { httpCode: 500, schema: ErrorSchema },
    { httpCode: 503, schema: ErrorSchema },
  ],
});

Benefits of Typed Handlers

Type Safety - Compiler catches type errors before runtime
Less Boilerplate - No manual parsing or validation code
Better IDE Support - IntelliSense shows exact types
Automatic Validation - Invalid requests rejected automatically
Consistent Error Responses - Standardized validation errors
Self-Documenting - Schemas serve as both validation and documentation


Registering Reusable Schemas

The app.openAPISchema() method registers Zod schemas as named types in the OpenAPI registry. This promotes reusability and keeps your OpenAPI specification clean by using references instead of inlining schemas everywhere.

Basic Schema Registration

Register a simple schema:

import { z } from "zod";

// Define schema
const UserSchema = z.object({
  id: z.string().uuid().describe("Unique user identifier"),
  name: z.string().min(1).max(100).describe("User full name"),
  email: z.string().email().describe("User email address"),
  createdAt: z.string().datetime().describe("Account creation timestamp"),
});

// Register schema with a name
app.openAPISchema('User', UserSchema);

// Now use it in endpoints
app.openAPIPath('GetUser', 'Get user by ID', {
  typedHandler: async ({ params }) => {
    const user = await getUserById(params.id);
    return { jsonBody: user };
  },
  methods: ["GET"],
  route: "users/{id}",
  params: z.object({ id: z.string().uuid() }),
  response: UserSchema, // References 'User' in OpenAPI spec
});

Nested Schema Registration

Register complex schemas with nested objects:

// Address schema
const AddressSchema = z.object({
  street: z.string().describe("Street address"),
  city: z.string().describe("City name"),
  state: z.string().length(2).describe("State code (2 letters)"),
  zipCode: z
    .string()
    .regex(/^\d{5}$/)
    .describe("5-digit ZIP code"),
});

app.openAPISchema('Address', AddressSchema);

// User profile with nested address
const UserProfileSchema = z.object({
  user: UserSchema,
  address: AddressSchema,
  phoneNumber: z.string().optional(),
  preferences: z.object({
    newsletter: z.boolean(),
    notifications: z.boolean(),
  }),
});

app.openAPISchema('UserProfile', UserProfileSchema);

Array Schemas

Register schemas for collections:

// Single todo item
const TodoSchema = z.object({
  id: z.string().uuid(),
  title: z.string(),
  completed: z.boolean(),
  createdAt: z.string().datetime(),
});

app.openAPISchema('Todo', TodoSchema);

// Paginated list
const PaginatedTodosSchema = z.object({
  items: z.array(TodoSchema).describe("List of todos"),
  total: z.number().int().describe("Total number of items"),
  page: z.number().int().describe("Current page number"),
  pageSize: z.number().int().describe("Items per page"),
  hasMore: z.boolean().describe("Whether more pages exist"),
});

app.openAPISchema('PaginatedTodos', PaginatedTodosSchema);

// Use in endpoint
app.openAPIPath('ListTodos', 'List all todos with pagination', {
  typedHandler: async ({ query }) => {
    const todos = await getTodos(query);
    return { jsonBody: todos };
  },
  methods: ["GET"],
  route: "todos",
  query: z.object({
    page: z.coerce.number().default(1),
    pageSize: z.coerce.number().default(10),
  }),
  response: PaginatedTodosSchema,
});

Error Schemas

Create standardized error responses:

// Base error schema
const ErrorSchema = z.object({
  error: z.string().describe("Error message"),
  code: z.string().describe("Error code"),
  timestamp: z.string().datetime().describe("When the error occurred"),
  path: z.string().optional().describe("Request path that caused the error"),
  details: z.any().optional().describe("Additional error details"),
});

app.openAPISchema('Error', ErrorSchema);

// Validation error schema
const ValidationErrorSchema = z.object({
  error: z.string(),
  code: z.literal("VALIDATION_ERROR"),
  issues: z.array(
    z.object({
      path: z.array(z.union([z.string(), z.number()])),
      message: z.string(),
    })
  ),
});

app.openAPISchema('ValidationError', ValidationErrorSchema);

// Use in endpoints
app.openAPIPath('CreateUser', 'Create new user', {
  typedHandler: async ({ body }) => {
    const user = await createUser(body);
    return { status: 201, jsonBody: user };
  },
  methods: ["POST"],
  route: "users",
  body: CreateUserSchema,
  responses: [
    {
      httpCode: 201,
      schema: UserSchema,
      description: "User created successfully",
    },
    {
      httpCode: 400,
      schema: ValidationErrorSchema,
      description: "Invalid request data",
    },
    {
      httpCode: 500,
      schema: ErrorSchema,
      description: "Internal server error",
    },
  ],
});

Discriminated Union Schemas

Register schemas with discriminated unions for polymorphic data:

// Base notification
const BaseNotificationSchema = z.object({
  id: z.string().uuid(),
  createdAt: z.string().datetime(),
});

// Email notification
const EmailNotificationSchema = BaseNotificationSchema.extend({
  type: z.literal("email"),
  to: z.string().email(),
  subject: z.string(),
  body: z.string(),
});

app.openAPISchema('EmailNotification', EmailNotificationSchema);

// SMS notification
const SmsNotificationSchema = BaseNotificationSchema.extend({
  type: z.literal("sms"),
  to: z.string().regex(/^\+?[1-9]\d{1,14}$/),
  message: z.string().max(160),
});

app.openAPISchema('SmsNotification', SmsNotificationSchema);

// Union of all notification types
const NotificationSchema = z.discriminatedUnion("type", [
  EmailNotificationSchema,
  SmsNotificationSchema,
]);

app.openAPISchema('Notification', NotificationSchema);

Schema Organization Best Practices

Organize by domain:

// schemas/user.ts
export const UserSchema = z.object({
  /* ... */
});
export const CreateUserSchema = z.object({
  /* ... */
});
export const UpdateUserSchema = z.object({
  /* ... */
});

// schemas/todo.ts
export const TodoSchema = z.object({
  /* ... */
});
export const CreateTodoSchema = z.object({
  /* ... */
});
export const UpdateTodoSchema = z.object({
  /* ... */
});

// schemas/index.ts
import { UserSchema, CreateUserSchema, UpdateUserSchema } from "./user";
import { TodoSchema, CreateTodoSchema, UpdateTodoSchema } from "./todo";

export function registerSchemas(app: typeof import("@azure/functions").app) {
  // User schemas
  app.openAPISchema('User', UserSchema);
  app.openAPISchema('CreateUser', CreateUserSchema);
  app.openAPISchema('UpdateUser', UpdateUserSchema);
  
  // Todo schemas
  app.openAPISchema('Todo', TodoSchema);
  app.openAPISchema('CreateTodo', CreateTodoSchema);
  app.openAPISchema('UpdateTodo', UpdateTodoSchema);
}

// In index.ts
import "@apvee/azure-functions-openapi";
import { app } from "@azure/functions";
import { registerSchemas } from "./schemas";

app.openAPISetup({ /* config */ });
registerSchemas(app);

Benefits of Schema Registration

DRY Principle - Define schemas once, use everywhere
Cleaner OpenAPI Spec - Uses $ref instead of inline schemas
Better Documentation - Named types are easier to understand
Consistency - Same schema used for validation and documentation
Type Reusability - Share types across multiple endpoints
Easier Maintenance - Update schema in one place

When to Register Schemas

Register schemas when:

  • Used in multiple endpoints
  • Complex nested structures
  • Part of your domain model
  • Error response formats
  • Common request/response patterns

Inline schemas when:

  • Used only once
  • Very simple structures
  • Endpoint-specific parameters
  • Quick prototyping

Example - Mixed approach:

// Register common schemas
app.openAPISchema('Todo', TodoSchema);
app.openAPISchema('Error', ErrorSchema);

app.openAPIPath('UpdateTodo', 'Update todo', {
  typedHandler: async ({ params, body }) => {
    const todo = await updateTodo(params.id, body);
    return { jsonBody: todo };
  },
  methods: ["PATCH"],
  route: "todos/{id}",
  // Inline simple param schema
  params: z.object({ id: z.string().uuid() }),
  // Inline endpoint-specific body schema
  body: z.object({
    title: z.string().optional(),
    completed: z.boolean().optional(),
  }),
  // Reference registered schema
  response: TodoSchema,
});

Webhooks (OpenAPI 3.1.0)

Webhooks are a new feature in OpenAPI 3.1.0 that allows you to document callback endpoints - HTTP requests that your API makes to external URLs. Unlike regular endpoints that clients call, webhooks represent notifications that your API sends to client-provided URLs.

Understanding Webhooks

Regular API Endpoint (Path):

Client → Your API
Example: GET /api/orders/123

Webhook (Callback):

Your API → Client's URL
Example: POST https://client.com/webhooks/order-completed

Basic Webhook Registration

Document a simple webhook notification:

import { z } from "zod";

// Define the payload your API will send
const OrderCompletedSchema = z.object({
  orderId: z.string().uuid().describe("Order identifier"),
  status: z.literal("completed").describe("Order status"),
  completedAt: z.string().datetime().describe("Completion timestamp"),
  totalAmount: z.number().describe("Total order amount"),
});

// Register webhook
app.openAPIWebhook('OrderCompleted', 'Notifies when an order is completed', {
  typedHandler: async ({ body, context }) => {
    // This is the handler that receives the webhook at YOUR endpoint
    // (for testing or development purposes)
    context.log(`Webhook received: Order ${body.orderId} completed`);
    return { jsonBody: { received: true } };
  },
  methods: ["POST"],
  body: OrderCompletedSchema,
  responses: [
    {
      httpCode: 200,
      description: "Webhook received successfully",
      schema: z.object({ received: z.boolean() }),
    },
  ],
});

Webhook with Authentication

Document webhooks that require authentication:

// Register security scheme for webhook signatures
const webhookSignature = app.openAPIKeySecurity(
  'X-Webhook-Signature',
  'header',
  'HMAC signature for webhook verification'
);

app.openAPIWebhook('PaymentProcessed', 'Notifies when payment is processed', {
  typedHandler: async ({ body, headers, context }) => {
    // Verify webhook signature
    const signature = headers["x-webhook-signature"];
    const isValid = verifyWebhookSignature(body, signature);

    if (!isValid) {
      return { status: 401, jsonBody: { error: "Invalid signature" } };
    }

    context.log(`Payment processed: ${body.paymentId}`);
    return { jsonBody: { acknowledged: true } };
  },
  methods: ["POST"],
  body: z.object({
    paymentId: z.string().uuid(),
    amount: z.number(),
    currency: z.string().length(3),
    status: z.enum(["success", "failed", "pending"]),
  }),
  headers: z.object({
    "x-webhook-signature": z.string(),
  }),
  security: [webhookSignature],
});

Multiple Webhook Events

Document different webhook events for various scenarios:

// User registration webhook
app.openAPIWebhook('UserRegistered', 'Notifies when a new user registers', {
  typedHandler: async ({ body, context }) => {
    context.log(`New user: ${body.userId}`);
    return { status: 200 };
  },
  methods: ["POST"],
  body: z.object({
    userId: z.string().uuid(),
    email: z.string().email(),
    registeredAt: z.string().datetime(),
  }),
  tags: ["User Events"],
});

// User deletion webhook
app.openAPIWebhook('UserDeleted', 'Notifies when a user is deleted', {
  typedHandler: async ({ body, context }) => {
    context.log(`User deleted: ${body.userId}`);
    return { status: 200 };
  },
  methods: ["POST"],
  body: z.object({
    userId: z.string().uuid(),
    deletedAt: z.string().datetime(),
    reason: z.string().optional(),
  }),
  tags: ["User Events"],
});

// Subscription events
app.openAPIWebhook('SubscriptionChanged', 'Notifies when subscription status changes', {
  typedHandler: async ({ body, context }) => {
    context.log(`Subscription ${body.subscriptionId} changed to ${body.status}`);
    return { status: 200 };
  },
  methods: ['POST'],
  body: z.object({
    subscriptionId: z.string().uuid(),
    userId: z.string().uuid(),
    status: z.enum(['active', 'cancelled', 'expired', 'paused']),
    changedAt: z.string().datetime()
  }),
  tags: ['Subscription Events']
});

Webhook Retry Logic Documentation

Document how your API handles webhook failures:

app.openAPIWebhook('OrderShipped', 'Notifies when order is shipped', {
  typedHandler: async ({ body, headers, context }) => {
    // Document retry attempt in description
    const attemptNumber = headers["x-webhook-attempt"];
    context.log(`Webhook attempt ${attemptNumber} for order ${body.orderId}`);

    return { status: 200 };
  },
  methods: ["POST"],
  description: `
Webhook sent when an order is shipped.

**Retry Policy:**
- Initial attempt immediately after shipping
- Retries after 1 min, 5 min, 15 min, 1 hour, 6 hours
- Maximum 5 retry attempts
- Exponential backoff applied

**Request Headers:**
- \`X-Webhook-Attempt\`: Retry attempt number (1-5)
- \`X-Webhook-Id\`: Unique webhook delivery ID
- \`X-Webhook-Timestamp\`: ISO 8601 timestamp
  `,
  body: z.object({
    orderId: z.string().uuid(),
    trackingNumber: z.string(),
    carrier: z.string(),
    shippedAt: z.string().datetime(),
    estimatedDelivery: z.string().datetime(),
  }),
  headers: z.object({
    "x-webhook-attempt": z.coerce.number().int().min(1).max(5),
    "x-webhook-id": z.string().uuid(),
    "x-webhook-timestamp": z.string().datetime(),
  }),
  responses: [
    {
      httpCode: 200,
      description: "Webhook acknowledged successfully",
    },
    {
      httpCode: 500,
      description: "Server error - webhook will be retried",
    },
  ],
});

Real-World Webhook Example: Stripe-style

Comprehensive webhook similar to Stripe's approach:

// Register webhook schemas
const WebhookEventSchema = z.object({
  id: z.string().uuid().describe("Unique event identifier"),
  type: z.string().describe("Event type"),
  created: z.number().int().describe("Unix timestamp"),
  data: z.object({
    object: z.any(),
  }),
  livemode: z.boolean().describe("Whether in production mode"),
});

app.openAPISchema('WebhookEvent', WebhookEventSchema);

// Webhook signature security
const stripeSignature = app.openAPIKeySecurity(
  'Stripe-Signature',
  'header',
  'Webhook signature for verification (see Stripe documentation)'
);

app.openAPIWebhook('StripeWebhook', 'Generic Stripe-style webhook endpoint', {
  typedHandler: async ({ body, headers, context }) => {
    const signature = headers["stripe-signature"];

    // Verify signature (pseudo-code)
    if (!verifyStripeSignature(body, signature, process.env.WEBHOOK_SECRET)) {
      return { status: 400, jsonBody: { error: "Invalid signature" } };
    }

    // Handle different event types
    context.log(`Event received: ${body.type}`);

    switch (body.type) {
      case "payment_intent.succeeded":
        await handlePaymentSuccess(body.data.object);
        break;
      case "payment_intent.payment_failed":
        await handlePaymentFailure(body.data.object);
        break;
      case "customer.subscription.created":
        await handleSubscriptionCreated(body.data.object);
        break;
      default:
        context.warn(`Unhandled event type: ${body.type}`);
    }

    return { jsonBody: { received: true } };
  },
  methods: ["POST"],
  description: `
Webhook endpoint for receiving events from Stripe.

**Verification:**
All webhook requests include a \`Stripe-Signature\` header. Verify this signature
using your webhook secret to ensure the request came from Stripe.

**Event Types:**
- \`payment_intent.succeeded\` - Payment completed successfully
- \`payment_intent.payment_failed\` - Payment failed
- \`customer.subscription.created\` - New subscription created
- \`customer.subscription.deleted\` - Subscription cancelled
- And many more...

**Best Practices:**
1. Always verify the signature
2. Return 2xx status code quickly
3. Process events asynchronously
4. Handle duplicate events (idempotency)
  `,
  body: WebhookEventSchema,
  headers: z.object({
    "stripe-signature": z.string().describe("HMAC signature of the payload"),
  }),
  security: [stripeSignature],
  responses: [
    {
      httpCode: 200,
      description: "Event received and queued for processing",
      schema: z.object({ received: z.boolean() }),
    },
    {
      httpCode: 400,
      description: "Invalid signature or malformed payload",
      schema: z.object({ error: z.string() }),
    },
  ],
  tags: ["Webhooks"],
});

Webhook Configuration Options

The app.openAPIWebhook() method accepts the same options as app.openAPIPath():

| Option | Description | | --------------------------- | -------------------------------------------- | | handler or typedHandler | Function to handle webhook (for testing) | | methods | HTTP methods (usually ['POST']) | | body | Expected webhook payload schema | | headers | Expected headers (signatures, IDs, etc.) | | security | Security requirements (signatures, API keys) | | responses | Possible response codes | | description | Detailed webhook documentation | | tags | Tags for organization |

Webhook vs Path Differences

Webhooks:

  • Documented in webhooks section of OpenAPI spec
  • Represent outbound requests from your API
  • Typically POST requests
  • Often include signature verification
  • Usually retry on failure

Paths:

  • Documented in paths section of OpenAPI spec
  • Represent inbound requests to your API
  • Support all HTTP methods
  • May require authentication
  • Client handles retries

Testing Webhooks Locally

Use tools like ngrok or webhook.site for local testing:

# Start ngrok tunnel
ngrok http 7071

# Your webhook URL becomes:
# https://abc123.ngrok.io/api/webhooks/order-completed

# Test with curl
curl -X POST https://abc123.ngrok.io/api/webhooks/order-completed \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "completed",
    "completedAt": "2025-11-06T12:00:00Z",
    "totalAmount": 99.99
  }'

🔒 Security Schemas

Security is a critical aspect of API development. This library provides native support for multiple Azure authentication methods and custom security schemes, all properly documented in your OpenAPI