Skip to content
Back to Blog
1 min read

Azure Static Web Apps: New Features at Build 2022

New Features Overview

Build 2022 brings several enhancements:

  • Stable APIs using Azure Functions
  • Enterprise edge support
  • Improved authentication options
  • Split environment configurations

Project Setup

Create a new Static Web App with React and Azure Functions:

# Create React app
npx create-react-app my-swa-app --template typescript
cd my-swa-app

# Create API folder
mkdir api
cd api
func init --worker-runtime node --language typescript
func new --name GetProducts --template "HTTP trigger"

API Implementation

Create a products API with Azure Functions:

// api/GetProducts/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { CosmosClient } from "@azure/cosmos";

const cosmosClient = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);
const database = cosmosClient.database("ecommerce");
const container = database.container("products");

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const category = req.query.category;
  const page = parseInt(req.query.page || "1");
  const pageSize = 20;

  let query = "SELECT * FROM c WHERE c.type = 'product'";
  const parameters: any[] = [];

  if (category) {
    query += " AND c.category = @category";
    parameters.push({ name: "@category", value: category });
  }

  query += " ORDER BY c.createdAt DESC";
  query += ` OFFSET ${(page - 1) * pageSize} LIMIT ${pageSize}`;

  const { resources: products } = await container.items
    .query({ query, parameters })
    .fetchAll();

  context.res = {
    status: 200,
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": "public, max-age=300",
    },
    body: {
      products,
      page,
      pageSize,
      hasMore: products.length === pageSize,
    },
  };
};

export default httpTrigger;

Authentication Configuration

Configure authentication providers:

// staticwebapp.config.json
{
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    },
    {
      "route": "/admin/*",
      "allowedRoles": ["admin"]
    },
    {
      "route": "/.auth/login/aad",
      "statusCode": 404
    }
  ],
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "userDetailsClaim": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
          "clientIdSettingName": "AAD_CLIENT_ID",
          "clientSecretSettingName": "AAD_CLIENT_SECRET"
        },
        "login": {
          "loginParameters": ["scope=openid profile email"]
        }
      },
      "customOpenIdConnectProviders": {
        "okta": {
          "registration": {
            "clientIdSettingName": "OKTA_CLIENT_ID",
            "clientCredential": {
              "clientSecretSettingName": "OKTA_CLIENT_SECRET"
            },
            "openIdConnectConfiguration": {
              "wellKnownOpenIdConfiguration": "https://dev-xxxxx.okta.com/.well-known/openid-configuration"
            }
          },
          "login": {
            "nameClaimType": "name",
            "scopes": ["openid", "profile", "email"]
          }
        }
      }
    }
  },
  "responseOverrides": {
    "401": {
      "redirect": "/.auth/login/aad",
      "statusCode": 302
    }
  },
  "globalHeaders": {
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY",
    "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
  }
}

React Frontend with Authentication

// src/components/UserProfile.tsx
import React, { useEffect, useState } from "react";

interface UserInfo {
  identityProvider: string;
  userId: string;
  userDetails: string;
  userRoles: string[];
  claims: { typ: string; val: string }[];
}

export const UserProfile: React.FC = () => {
  const [user, setUser] = useState<UserInfo | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch("/.auth/me");
        const data = await response.json();
        if (data.clientPrincipal) {
          setUser(data.clientPrincipal);
        }
      } catch (error) {
        console.error("Failed to fetch user", error);
      } finally {
        setLoading(false);
      }
    }
    fetchUser();
  }, []);

  if (loading) return <div>Loading...</div>;

  if (!user) {
    return (
      <div className="auth-buttons">
        <a href="/.auth/login/aad" className="btn btn-primary">
          Login with Azure AD
        </a>
        <a href="/.auth/login/github" className="btn btn-secondary">
          Login with GitHub
        </a>
      </div>
    );
  }

  return (
    <div className="user-profile">
      <h2>Welcome, {user.userDetails}</h2>
      <p>Provider: {user.identityProvider}</p>
      <p>Roles: {user.userRoles.join(", ")}</p>
      <a href="/.auth/logout" className="btn btn-outline">
        Logout
      </a>
    </div>
  );
};

Products Component

// src/components/ProductList.tsx
import React, { useEffect, useState } from "react";

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  imageUrl: string;
  category: string;
}

interface ProductsResponse {
  products: Product[];
  page: number;
  hasMore: boolean;
}

export const ProductList: React.FC<{ category?: string }> = ({ category }) => {
  const [products, setProducts] = useState<Product[]>([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);

  const fetchProducts = async (pageNum: number) => {
    setLoading(true);
    try {
      const params = new URLSearchParams({ page: pageNum.toString() });
      if (category) params.append("category", category);

      const response = await fetch(`/api/GetProducts?${params}`);
      const data: ProductsResponse = await response.json();

      setProducts((prev) =>
        pageNum === 1 ? data.products : [...prev, ...data.products]
      );
      setHasMore(data.hasMore);
    } catch (error) {
      console.error("Failed to fetch products", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    setPage(1);
    fetchProducts(1);
  }, [category]);

  const loadMore = () => {
    const nextPage = page + 1;
    setPage(nextPage);
    fetchProducts(nextPage);
  };

  return (
    <div className="product-list">
      <div className="products-grid">
        {products.map((product) => (
          <div key={product.id} className="product-card">
            <img src={product.imageUrl} alt={product.name} />
            <h3>{product.name}</h3>
            <p>{product.description}</p>
            <span className="price">${product.price.toFixed(2)}</span>
          </div>
        ))}
      </div>
      {hasMore && (
        <button onClick={loadMore} disabled={loading} className="load-more">
          {loading ? "Loading..." : "Load More"}
        </button>
      )}
    </div>
  );
};

GitHub Actions Deployment

# .github/workflows/azure-static-web-apps.yml
name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main

jobs:
  build_and_deploy:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          REACT_APP_API_URL: "/api"

      - name: Build API
        run: |
          cd api
          npm ci
          npm run build

      - name: Deploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "/"
          api_location: "api"
          output_location: "build"

  close_pull_request:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Close Pull Request
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          action: "close"

Enterprise Edge Configuration

Enable enterprise-grade CDN:

resource staticWebApp 'Microsoft.Web/staticSites@2022-03-01' = {
  name: 'my-swa'
  location: 'eastus2'
  sku: {
    name: 'Standard'
    tier: 'Standard'
  }
  properties: {
    repositoryUrl: 'https://github.com/org/repo'
    branch: 'main'
    buildProperties: {
      appLocation: '/'
      apiLocation: 'api'
      outputLocation: 'build'
    }
    enterpriseGradeCdnStatus: 'Enabled'
  }
}

resource customDomain 'Microsoft.Web/staticSites/customDomains@2022-03-01' = {
  parent: staticWebApp
  name: 'www.contoso.com'
  properties: {
    validationMethod: 'cname-delegation'
  }
}

Summary

Azure Static Web Apps at Build 2022 offers:

  • Stable Azure Functions integration for APIs
  • Enhanced authentication with custom providers
  • Enterprise edge for global distribution
  • Improved configuration options
  • Seamless GitHub/Azure DevOps integration

It remains an excellent choice for JAMstack applications with serverless backends.

Michael John Peña

Michael John Peña

Data & AI Director and Microsoft MVP based in Sydney. Writing about Microsoft Fabric, Azure, data engineering, and AI.