Azure Static Web Apps Is the Most Underrated Service for Developers
Why Azure Static Web Apps should be your default for frontend deployments — free tier, zero-config CI/CD, built-in auth, and API routes included.
Every Azure developer I talk to has the same default: App Service. React app? App Service. Static marketing site? App Service. Documentation portal? App Service.
I was the same way. But honestly, for static frontends and SPAs, App Service is overkill. And you're paying for that overkill every month.
This post walks through Azure Static Web Apps -- what it actually is, how to deploy one from scratch, and why the free tier is genuinely production-ready.
Azure Static Web Apps gives you global CDN hosting, auto CI/CD from GitHub, built-in authentication, and serverless API routes -- on a free tier that's actually usable in production. If you're deploying static frontends to App Service, you're overengineering it.
The Problem: App Service for Everything
Here's what deploying a static frontend to App Service looks like:
# Step 1: Create an App Service Plan
az appservice plan create \
--name my-frontend-plan \
--resource-group my-rg \
--sku B1 \
--is-linux
# Step 2: Create the Web App
az webapp create \
--name my-frontend-app \
--resource-group my-rg \
--plan my-frontend-plan \
--runtime "NODE:20-lts"
# Step 3: Configure deployment source
az webapp deployment source config \
--name my-frontend-app \
--resource-group my-rg \
--repo-url https://github.com/you/your-repo \
--branch main
# Step 4: Configure custom domain
az webapp config hostname add \
--webapp-name my-frontend-app \
--resource-group my-rg \
--hostname www.yourdomain.com
# Step 5: Add SSL
az webapp config ssl bind \
--name my-frontend-app \
--resource-group my-rg \
--certificate-thumbprint <thumbprint> \
--ssl-type SNIFive commands. An App Service Plan you're paying for 24/7. SSL management. No CDN -- that's another service. The B1 plan starts at ~$13/month on Linux. For serving static files.
What Static Web Apps Actually Is
Not just "static file hosting." It's a complete frontend deployment platform:
- Global CDN -- your content is served from edge nodes worldwide
- Automatic CI/CD -- push to GitHub, your site deploys
- Free SSL certificates -- automatic, no configuration
- Custom domains -- point your DNS and you're done
- Built-in authentication -- Entra ID, GitHub, Twitter out of the box
- API routes -- serverless functions alongside your static content
- Preview environments -- every PR gets its own deployment
The truth is, Microsoft built this service to compete with Vercel and Netlify. And for static/SPA workloads, it does exactly that.
Deploy Your First Static Web App
Let's go from zero to live site. Azure CLI installed, GitHub repo ready.
# Login if you haven't
az login
# Create a resource group (or use an existing one)
az group create \
--name swa-demo-rg \
--location eastus2
# Create the Static Web App
az staticwebapp create \
--name my-first-swa \
--resource-group swa-demo-rg \
--source https://github.com/your-username/your-repo \
--location eastus2 \
--branch main \
--app-location "/" \
--output-location "build" \
--login-with-githubOne command. Azure connects to your repo, generates a GitHub Actions workflow, triggers the first build, and gives you a live URL at https://<random-name>.azurestaticapps.net. No App Service Plan. No SSL config. No CDN setup.
Local Development with the SWA CLI
# Install the SWA CLI
npm install -g @azure/static-web-apps-cli
# Start local dev server with auth + API emulation
swa init
swa start ./build --api-location ./api
# Deploy directly from CLI (alternative to GitHub Actions)
swa deploy ./build --deployment-token <your-token> --env productionThe swa start command emulates auth and proxies API requests locally. What you see in dev is what you get in production.
The Free Tier Reality
No credit card tricks, no "free for 12 months" caveats:
| Feature | Free Plan | Standard Plan |
|---|---|---|
| Bandwidth | 100 GB/month | 100 GB/month (+ $0.20/GB overage) |
| Storage per environment | 250 MB | 500 MB |
| Total storage | 500 MB | 2 GB |
| Custom domains | 2 | 6 |
| Preview environments | 3 | 10 |
| Apps per subscription | 10 | 100 |
| SSL certificates | Included | Included |
| Global CDN | Included | Included |
| CI/CD | Included | Included |
| Built-in auth (pre-configured) | Included | Included |
| Custom auth (OpenID Connect) | Not included | Included |
| Private endpoints | Not included | Included |
| SLA | None | 99.95% |
| Price | $0/month | ~$9/month per app |
100 GB of bandwidth handles roughly 200,000-500,000 page views per month. For portfolio sites, documentation, and most SPAs, you'll never come close.
SWA Free vs App Service Basic -- The Honest Comparison
| Capability | SWA Free | App Service B1 |
|---|---|---|
| Monthly cost | $0 | ~$13 (Linux) |
| Global CDN | Yes | No (need Azure CDN separately) |
| Auto SSL | Yes | Manual or App Service Managed Certificate |
| CI/CD setup | Zero-config (auto-generated) | Manual GitHub Actions or Azure DevOps |
| Custom domains | 2 | Custom (depends on plan) |
| Built-in auth | Entra ID, GitHub, Twitter | None (code it yourself) |
| Serverless APIs | Managed Functions included | Separate resource needed |
| Preview environments | 3 per app | Deployment slots (not on Basic) |
| Server-side rendering | Limited | Full support |
| WebSockets | Not supported | Supported |
| Custom runtime | No | Yes |
Built-in Authentication
This is the feature that honestly sold me. Instead of identity providers, callback handlers, and token management -- you configure auth in a single JSON file:
{
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"clientIdSettingName": "AAD_CLIENT_ID",
"clientSecretSettingName": "AAD_CLIENT_SECRET"
}
}
}
},
"routes": [
{
"route": "/login",
"rewrite": "/.auth/login/aad"
},
{
"route": "/dashboard/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/admin/*",
"allowedRoles": ["admin"]
}
],
"responseOverrides": {
"401": {
"statusCode": 302,
"redirect": "/login"
}
}
}Save this as staticwebapp.config.json in the root of your app. That's it. No auth libraries. No token management. No callback URLs.
The pre-configured providers (GitHub, Entra ID, Twitter) work on the Free plan. Route users to /.auth/login/github and SWA handles the entire OAuth flow. Custom OpenID Connect providers (Google, Apple, Auth0) require the Standard plan.
Accessing User Info in Your App
Once a user is authenticated, their info is available at a built-in endpoint:
// In your frontend code
async function getUserInfo() {
const response = await fetch('/.auth/me');
const { clientPrincipal } = await response.json();
if (clientPrincipal) {
console.log(`Logged in as: ${clientPrincipal.userDetails}`);
console.log(`Provider: ${clientPrincipal.identityProvider}`);
console.log(`Roles: ${clientPrincipal.userRoles.join(', ')}`);
}
return clientPrincipal;
}No JWT decoding. No auth middleware. The platform does it for you.
API Routes with Azure Functions
Create an api folder in your repo and you've got a serverless backend.
my-app/
src/ # Your frontend code
api/
package.json
host.json
GetProducts/
function.json
index.js
staticwebapp.config.json
A Simple API Function
// api/GetProducts/index.js
module.exports = async function (context, req) {
const products = [
{ id: 1, name: "Azure Fundamentals Lab", price: 0, tier: "free" },
{ id: 2, name: "Advanced Networking Lab", price: 29, tier: "premium" },
{ id: 3, name: "Security Operations Lab", price: 29, tier: "premium" }
];
// Access authenticated user info (passed automatically by SWA)
const header = req.headers["x-ms-client-principal"];
let user = null;
if (header) {
const encoded = Buffer.from(header, "base64");
user = JSON.parse(encoded.toString("ascii"));
}
// Filter based on user role
const filtered = user?.userRoles?.includes("premium")
? products
: products.filter(p => p.tier === "free");
context.res = {
status: 200,
headers: { "Content-Type": "application/json" },
body: filtered
};
};// api/GetProducts/function.json
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get"],
"route": "products"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}Your frontend calls /api/products and it just works. No CORS. No separate deployment.
Managed Functions limits: HTTP triggers only, 45-second timeout, consumption plan (cold starts), no Durable Functions. The Standard plan lets you "bring your own" Functions app with no restrictions. But managed functions cover 90% of API-behind-frontend use cases.
GitHub Actions Integration
Azure auto-generates this workflow file when you create a Static Web App connected to GitHub:
# .github/workflows/azure-static-web-apps-<random-name>.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_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v4
with:
submodules: true
lfs: false
- name: Build And Deploy
id: builddeploy
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 source code path
app_location: "/"
# API source code path - optional
api_location: "api"
# Built app content directory
output_location: "build"
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: "close"Notice what you didn't have to write: no build config (auto-detected), no deployment credentials (injected automatically), no CDN invalidation, no SSL renewal. Preview environments are created for every PR and cleaned up automatically by the close_pull_request_job.
You can customize the workflow, but the default works out of the box. Zero config really means zero config.
When NOT to Use Static Web Apps
The truth is it's not for everything. Here's when you should reach for something else:
- SSR-heavy apps -- Next.js/Nuxt.js with full SSR needs a running Node.js server. Use App Service.
- WebSockets -- real-time chat, live dashboards, collaborative editing. Use App Service.
- Custom runtimes -- Python, Go, Java backends that aren't Functions-compatible. Use App Service.
- Microservices / containers -- multiple backend services, KEDA scaling, Docker workflows. Use Container Apps or AKS.
- Pure static, no APIs -- just HTML behind a CDN with no auth needed. Azure Storage Static Website is even simpler.
The Decision Framework
- Does my frontend need a server running? If yes, skip SWA.
- Do I need more than HTTP-triggered APIs? If yes, use "bring your own" Functions on Standard, or App Service.
- Am I serving more than 100 GB/month? If yes, upgrade to Standard or evaluate your architecture.
All three "no"? Static Web Apps is your answer.
Try It Yourself
Check out the CloudLearn Static Web Apps lab for a hands-on walkthrough with auth and API routes. Or just pick a side project, run az staticwebapp create, and see what happens.

CloudLearn Lab: Deploy Your First Website with Azure CLI
Build and deploy a real Azure Static Web App step by step using Azure CLI.
You'll wonder why you were paying for App Service.
Read Next
Azure Deployment Stacks Change Everything About Resource Lifecycle
How Deployment Stacks solve the resource orphan problem that Bicep and ARM alone can't — with DenySettings, managed cleanup, and lifecycle awareness.
How to Use Azure App Service Deployment Slots for Zero-Downtime Releases
Step-by-step guide to setting up deployment slots in Azure App Service. Deploy to staging, validate, and swap to production with zero downtime.