Azure Functions

Deploy your Vercube application to Azure Functions with HTTP triggers

Deploy your Vercube application to Azure Functions with complete support for HTTP triggers, efficient streaming, and seamless cookie management.

Basic Setup

src/functions/httpTrigger.ts
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { toServerlessHandler } from '@vercube/serverless/azure-functions';
import { app as vercubeApp } from '../index';

const handler = toServerlessHandler(vercubeApp);

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  return await handler(request);
}

app.http('httpTrigger', {
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
  authLevel: 'anonymous',
  handler: httpTrigger,
  route: '{*route}' // Catch all routes
});

Request Conversion

The adapter converts Azure Functions HttpRequest to standard web Request:

Input Format

// Azure HttpRequest
{
  method: 'GET',
  url: 'https://myapp.azurewebsites.net/api/users?page=1&limit=10',
  headers: {
    'content-type': 'application/json',
    'authorization': 'Bearer token...',
    'cookie': 'session_id=abc123; theme=dark'
  },
  query: {
    page: '1',
    limit: '10'
  },
  params: {
    id: '123'
  },
  body: ReadableStream { ... }
}

Output Format

// Standard Request
{
  method: 'GET',
  url: 'https://myapp.azurewebsites.net/api/users?page=1&limit=10',
  headers: Headers {
    'content-type': 'application/json',
    'authorization': 'Bearer token...',
    'cookie': 'session_id=abc123; theme=dark'
  },
  body: ReadableStream { ... }
}

Response Conversion with Streaming

Azure Functions responses use AsyncIterableIterator for efficient data streaming:

@Controller('/data')
export class DataController {
  
  @Inject(DataService)
  private dataService!: DataService;
  
  @Get('/export')
  async exportData() {
    const data = await this.dataService.getLargeDataset();
    
    return new Response(JSON.stringify(data), {
      headers: {
        'Content-Type': 'application/json',
        'Content-Disposition': 'attachment; filename="export.json"'
      }
    });
  }
  
  @Get('/stream')
  async streamData() {
    const stream = this.dataService.createDataStream();
    
    return new Response(stream, {
      headers: {
        'Content-Type': 'application/json',
        'Transfer-Encoding': 'chunked'
      }
    });
  }
}

The adapter automatically:

  1. Reads the Response body
  2. Converts it to AsyncIterableIterator<Uint8Array>
  3. Sets proper headers
  4. Streams data efficiently to the client

Cookies are properly handled through Set-Cookie headers:

@Controller('/auth')
export class AuthController {
  
  @Inject(AuthService)
  private authService!: AuthService;
  
  @Post('/login')
  async login(@Body({ validationSchema: LoginSchema }) credentials: LoginDto) {
    const token = await this.authService.generateToken(credentials);
    
    // Set authentication cookie
    return FastResponse.ok({ success: true })
      .cookie('session_id', token, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 3600
      })
      .cookie('user_preferences', 'theme=dark', {
        maxAge: 86400
      });
  }
  
  @Post('/logout')
  async logout() {
    // Clear authentication cookie
    return FastResponse.ok({ success: true })
      .cookie('session_id', '', {
        httpOnly: true,
        secure: true,
        maxAge: 0
      });
  }
  
  @Get('/session')
  async getSession(@Cookie('session_id') sessionId: string) {
    if (!sessionId) {
      throw new UnauthorizedException('No active session');
    }
    
    const session = await this.authService.validateSession(sessionId);
    return { session };
  }
}

How it works:

  • Multiple cookies via multiple Set-Cookie headers
  • Proper cookie attribute handling (HttpOnly, Secure, SameSite)
  • Cookie parsing from request headers

Azure-Specific Features

Invocation Context

Access Azure Functions execution context:

import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { toServerlessHandler } from '@vercube/serverless/azure-functions';
import { app as vercubeApp } from '../index';

const handler = toServerlessHandler(vercubeApp);

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  // Log invocation details
  context.log('HTTP trigger function processed request');
  context.log('Request URL:', request.url);
  context.log('Request method:', request.method);
  context.log('Invocation ID:', context.invocationId);
  
  // Set trace context
  context.traceContext.traceparent = request.headers.get('traceparent');
  
  try {
    const result = await handler(request);
    context.log('Request completed successfully');
    return result;
  } catch (error) {
    context.error('Request failed:', error);
    throw error;
  }
}

app.http('httpTrigger', {
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
  authLevel: 'anonymous',
  handler: httpTrigger,
  route: '{*route}'
});

Environment Variables

Access Azure Functions environment variables:

import { RuntimeConfig } from '@vercube/core';

@Controller('/config')
export class ConfigController {
  
  @Get('/azure-info')
  getAzureInfo() {
    return {
      // Azure Functions environment variables
      siteName: process.env.WEBSITE_SITE_NAME,
      resourceGroup: process.env.WEBSITE_RESOURCE_GROUP,
      region: process.env.REGION_NAME,
      instanceId: process.env.WEBSITE_INSTANCE_ID,
      hostname: process.env.WEBSITE_HOSTNAME,
      
      // Function execution details
      functionsVersion: process.env.FUNCTIONS_EXTENSION_VERSION,
      workerRuntime: process.env.FUNCTIONS_WORKER_RUNTIME
    };
  }
  
  @Get('/app-settings')
  getAppSettings() {
    return {
      nodeEnv: process.env.NODE_ENV,
      databaseUrl: process.env.DATABASE_URL ? 'configured' : 'not configured',
      customSetting: process.env.CUSTOM_SETTING
    };
  }
}

Function Configuration

Basic host.json

host.json
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "maxTelemetryItemsPerSecond": 20,
        "excludedTypes": "Request"
      }
    },
    "logLevel": {
      "default": "Information",
      "Function": "Information"
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

Advanced host.json

host.json
{
  "version": "2.0",
  
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "maxTelemetryItemsPerSecond": 20
      },
      "enableDependencyTracking": true
    },
    "logLevel": {
      "default": "Information",
      "Host.Results": "Error",
      "Function.httpTrigger": "Debug"
    }
  },
  
  "http": {
    "routePrefix": "api",
    "maxOutstandingRequests": 200,
    "maxConcurrentRequests": 100,
    "dynamicThrottlesEnabled": true
  },
  
  "functionTimeout": "00:05:00",
  
  "healthMonitor": {
    "enabled": true,
    "healthCheckInterval": "00:00:10",
    "healthCheckWindow": "00:02:00",
    "healthCheckThreshold": 6,
    "counterThreshold": 0.80
  },
  
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

Function-Specific Configuration

src/functions/httpTrigger.ts
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { toServerlessHandler } from '@vercube/serverless/azure-functions';
import { app as vercubeApp } from '../index';

const handler = toServerlessHandler(vercubeApp);

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  return await handler(request);
}

app.http('httpTrigger', {
  // HTTP methods
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
  
  // Authentication level
  authLevel: 'anonymous', // or 'function', 'admin'
  
  // Route pattern
  route: '{*route}',
  
  // Handler function
  handler: httpTrigger
});

// Add more function endpoints
app.http('adminTrigger', {
  methods: ['GET', 'POST'],
  authLevel: 'admin',
  route: 'admin/{*route}',
  handler: async (request: HttpRequest) => {
    // Admin-only endpoint
    return await handler(request);
  }
});

Database Connections

Handle database connections efficiently in Azure Functions:

Connection Pooling

export class DatabaseService {
  private static pool: Pool | null = null;
  
  async getPool() {
    // Reuse connection pool across invocations
    if (DatabaseService.pool) {
      return DatabaseService.pool;
    }
    
    DatabaseService.pool = new Pool({
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT),
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      
      // Azure Functions optimized settings
      max: 10,  // Higher than Lambda due to concurrent request handling
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000
    });
    
    return DatabaseService.pool;
  }
  
  async query(sql: string, params?: any[]) {
    const pool = await this.getPool();
    return await pool.query(sql, params);
  }
}

Azure SQL Database

import sql from 'mssql';

export class AzureSqlService {
  private static pool: sql.ConnectionPool | null = null;
  
  async getPool() {
    if (AzureSqlService.pool) {
      return AzureSqlService.pool;
    }
    
    const config = {
      server: process.env.AZURE_SQL_SERVER!,
      database: process.env.AZURE_SQL_DATABASE!,
      authentication: {
        type: 'azure-active-directory-default' as const
      },
      options: {
        encrypt: true,
        enableArithAbort: true
      },
      pool: {
        max: 10,
        min: 0,
        idleTimeoutMillis: 30000
      }
    };
    
    AzureSqlService.pool = await sql.connect(config);
    return AzureSqlService.pool;
  }
  
  async query(queryText: string) {
    const pool = await this.getPool();
    const result = await pool.request().query(queryText);
    return result.recordset;
  }
}

Application Insights Integration

Enable Application Insights

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsStorage": "",
    "APPINSIGHTS_INSTRUMENTATIONKEY": "your-instrumentation-key",
    "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=your-key;..."
  }
}

Custom Telemetry

import { TelemetryClient } from 'applicationinsights';

const telemetry = new TelemetryClient(
  process.env.APPLICATIONINSIGHTS_CONNECTION_STRING
);

@Controller('/tracked')
export class TrackedController {
  
  @Post('/order')
  async createOrder(@Body() order: OrderDto) {
    const startTime = Date.now();
    
    try {
      // Track custom event
      telemetry.trackEvent({
        name: 'OrderCreated',
        properties: {
          userId: order.userId,
          items: order.items.length
        }
      });
      
      const result = await this.orderService.create(order);
      
      // Track custom metric
      telemetry.trackMetric({
        name: 'OrderProcessingTime',
        value: Date.now() - startTime
      });
      
      return result;
    } catch (error) {
      // Track exception
      telemetry.trackException({
        exception: error,
        properties: {
          userId: order.userId
        }
      });
      
      throw error;
    }
  }
}

Advanced Response Handling

Custom Headers

@Controller('/api')
export class ApiController {
  
  @Get('/cached-data')
  getCachedData() {
    return FastResponse.ok({ data: 'cached' })
      .header('Cache-Control', 'public, max-age=3600')
      .header('X-Custom-Header', 'custom-value')
      .header('Access-Control-Allow-Origin', '*');
  }
  
  @Get('/download')
  downloadFile() {
    const fileContent = Buffer.from('file content');
    
    return new Response(fileContent, {
      headers: {
        'Content-Type': 'application/octet-stream',
        'Content-Disposition': 'attachment; filename="file.txt"',
        'Content-Length': fileContent.length.toString()
      }
    });
  }
}

Error Handling

import { NotFoundException, BadRequestException, InternalServerErrorException } from '@vercube/core';
import { InvocationContext } from '@azure/functions';

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  try {
    return await handler(request);
  } catch (error) {
    // Log to Azure
    context.error('Request failed:', error);
    
    // Return appropriate error response
    if (error instanceof NotFoundException) {
      return {
        status: 404,
        jsonBody: {
          statusCode: 404,
          message: error.message
        }
      };
    }
    
    return {
      status: 500,
      jsonBody: {
        statusCode: 500,
        message: 'Internal server error'
      }
    };
  }
}

Deployment

Deploy Using Azure CLI

# Login to Azure
az login

# Create resource group
az group create --name MyResourceGroup --location eastus

# Create storage account
az storage account create \
  --name mystorageaccount \
  --resource-group MyResourceGroup \
  --location eastus \
  --sku Standard_LRS

# Create function app
az functionapp create \
  --name MyFunctionApp \
  --resource-group MyResourceGroup \
  --storage-account mystorageaccount \
  --consumption-plan-location eastus \
  --runtime node \
  --runtime-version 18 \
  --functions-version 4

# Deploy
func azure functionapp publish MyFunctionApp

Deploy Using Azure Functions Core Tools

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

# Initialize function app
func init --worker-runtime node --language typescript

# Start local development
func start

# Deploy to Azure
func azure functionapp publish <APP_NAME>

Deploy with GitHub Actions

.github/workflows/deploy.yml
name: Deploy to Azure Functions

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Deploy to Azure Functions
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ secrets.AZURE_FUNCTIONAPP_NAME }}
          package: .
          publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}

Environment Configuration

Local Development

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    
    // Application settings
    "NODE_ENV": "development",
    "DATABASE_URL": "postgresql://localhost:5432/myapp",
    "REDIS_URL": "redis://localhost:6379",
    "JWT_SECRET": "dev-secret",
    
    // Azure services
    "AZURE_STORAGE_CONNECTION_STRING": "...",
    "COSMOS_DB_ENDPOINT": "...",
    "COSMOS_DB_KEY": "...",
    
    // Application Insights
    "APPINSIGHTS_INSTRUMENTATIONKEY": "...",
    "APPLICATIONINSIGHTS_CONNECTION_STRING": "..."
  },
  "Host": {
    "CORS": "*",
    "CORSCredentials": false
  }
}

Production Configuration

Configure application settings in Azure Portal:

# Using Azure CLI
az functionapp config appsettings set \
  --name MyFunctionApp \
  --resource-group MyResourceGroup \
  --settings \
    NODE_ENV=production \
    DATABASE_URL="postgresql://..." \
    JWT_SECRET="production-secret"

Monitoring and Debugging

Application Insights Queries

// Query failed requests
requests
| where success == false
| where timestamp > ago(1h)
| project timestamp, name, resultCode, duration
| order by timestamp desc

// Query slow requests
requests
| where duration > 1000
| where timestamp > ago(1h)
| project timestamp, name, duration, resultCode
| order by duration desc

// Query exceptions
exceptions
| where timestamp > ago(1h)
| project timestamp, type, outerMessage, innerMessage
| order by timestamp desc

// Custom events
customEvents
| where name == "OrderCreated"
| where timestamp > ago(24h)
| summarize count() by bin(timestamp, 1h)

Live Metrics

Monitor your function in real-time:

# View live metrics in portal
# Navigate to: Function App > Monitoring > Live Metrics

Log Streaming

# Stream logs using Azure CLI
az webapp log tail \
  --name MyFunctionApp \
  --resource-group MyResourceGroup

# Or using Core Tools
func azure functionapp logstream MyFunctionApp

Performance Optimization

Cold Start Reduction

// Initialize at module level
import { createApp } from '@vercube/core';
import { toServerlessHandler } from '@vercube/serverless/azure-functions';

const app = createApp({
  setup: async (app) => {
    // Initialize heavy services once
    app.container.bind(DatabaseService);
    app.container.bind(CacheService);
    
    const db = app.container.get(DatabaseService);
    await db.connect();
  }
});

const handler = toServerlessHandler(app);

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  // Fast execution - just handle the request
  return await handler(request);
}

Always-On Setting

Enable Always On to prevent cold starts:

# Using Azure CLI
az functionapp config set \
  --name MyFunctionApp \
  --resource-group MyResourceGroup \
  --always-on true

Premium Plan

Use Azure Functions Premium Plan for better performance:

# Create Premium plan
az functionapp plan create \
  --name MyPremiumPlan \
  --resource-group MyResourceGroup \
  --location eastus \
  --sku EP1 \
  --is-linux false

# Create function app with Premium plan
az functionapp create \
  --name MyFunctionApp \
  --resource-group MyResourceGroup \
  --plan MyPremiumPlan \
  --runtime node \
  --runtime-version 18

Troubleshooting

Common Issues

Function not triggering

Error: No HTTP triggers found

Solution: Ensure route configuration is correct:

app.http('httpTrigger', {
  route: '{*route}',  // Catch all routes
  handler: httpTrigger
});

CORS errors

Access to fetch has been blocked by CORS policy

Solution: Configure CORS in host.json:

{
  "version": "2.0",
  "extensions": {
    "http": {
      "routePrefix": "api",
      "cors": {
        "allowedOrigins": ["*"],
        "allowedMethods": ["GET", "POST", "PUT", "DELETE"],
        "allowedHeaders": ["*"]
      }
    }
  }
}

Request timeout

Function execution timed out

Solution: Increase timeout in host.json:

{
  "functionTimeout": "00:10:00"
}

Memory issues

JavaScript heap out of memory

Solution: Increase memory by upgrading plan or optimizing code

Connection string not found

AzureWebJobsStorage connection string not found

Solution: Set storage connection string:

{
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;..."
  }
}

Best Practices

Use Application Insights for monitoring

// Always enable Application Insights
const telemetry = new TelemetryClient();
telemetry.trackEvent({ name: 'CustomEvent' });

Handle concurrent requests efficiently

// Use proper connection pooling
const pool = new Pool({
  max: 10,  // Azure Functions can handle multiple concurrent requests
  idleTimeoutMillis: 30000
});

Implement proper error handling

export async function httpTrigger(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  try {
    return await handler(request);
  } catch (error) {
    context.error('Error:', error);
    return {
      status: 500,
      jsonBody: { error: 'Internal server error' }
    };
  }
}

Use managed identity for Azure services

// Instead of connection strings, use managed identity
import { DefaultAzureCredential } from '@azure/identity';

const credential = new DefaultAzureCredential();
const client = new BlobServiceClient(
  `https://${accountName}.blob.core.windows.net`,
  credential
);
Previous

Overview

Flexible and extensible storage system for Vercube applications

Next