Serverless Portfolio with Azure Functions
Build a live portfolio site with a Python serverless API on Azure.
Introduction
30 Second Summary
Every developer needs a way to showcase their work online. Imagine a portfolio site that pulls your resume data live from an API you built yourself.
In this project, you will build a portfolio website with a Python-powered serverless backend and deploy it to Azure Static Web Apps. Every time you push code to GitHub, your site redeploys automatically.
What You'll Build
Share your live URL with a recruiter, and they will see your portfolio page with resume data pulled straight from a serverless API you wrote and deployed yourself.
By the end of this project, you'll have:
- A portfolio landing page built with HTML/CSS that fetches live data from your own serverless API.
- A Python Azure Function at /api/resume that serves your resume as structured JSON.
- A live URL on azurestaticapps.net with free SSL, auto-deployed on every push through GitHub Actions.
- Secret Mission: Add a second /api/projects endpoint and wire the frontend to display your project portfolio dynamically.
Are there any prerequisites?
No prior experience with Azure or serverless development is needed. This guide walks you through setting up every tool from scratch.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
You're going to build a live portfolio website with a real serverless API behind it, deployed straight from GitHub to Azure Static Web Apps. By the end, you'll have a shareable URL that anyone can visit to see your portfolio page and hit your API endpoint to get your resume back as structured JSON.
Set Up Your Tools and Repository
Before you can build a serverless portfolio site, you need the right tools on your machine and a place to store your code.
This step gets Python, Git, and Azure Functions Core Tools installed. Then you will create a GitHub repository and scaffold the project folder structure so everything is ready for coding.
In this step, get ready to:
- Install and verify Python 3.11+, Git, and Azure Functions Core Tools v4.
- Create a GitHub repository and clone it locally with the project folder structure.
- Initialize the API folder as a Python Azure Functions project and verify it starts.
Check and install prerequisites
This project depends on three command-line tools. You will check whether each one is already installed and install anything that is missing.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your Python version by running this command:
python3 --version
✔️ I see version 3.11 or higher
Python is ready. Move on to the Git check below.
ⓧ I see an older version
Your system has an older Python that will not work with this project. macOS often ships with an older default Python.
- Download and install Python 3.11 or newer from python.org/downloads.
- After the installer finishes, close and reopen your terminal.
- Run python3 --version again and confirm it now shows 3.11 or higher.
macOS default Python
macOS may still resolve python3 to an older system version even after you install a newer one. If that happens, try running python3.11 --version instead.
You can also use a version manager like pyenv to switch between versions.
ⓧ Command not found
- Download and install Python 3.11 or newer from python.org/downloads.
- Follow the installer prompts and make sure the option to add Python to your PATH is checked (Windows).
- Close and reopen your terminal.
- Run python3 --version again and confirm it shows 3.11 or higher.
- Check your Git version by running this command:
git --version
✔️ I see a version number
Git is ready. Move on to the Azure Functions Core Tools check below.
ⓧ Command not found
Install Git for your operating system.
macOS
- Install Git using Homebrew by running:
brew install git
- If you do not have Homebrew, you can also install Git by running xcode-select --install in your terminal to get the Xcode Command Line Tools.
Windows
- Download and run the official installer from git-scm.com/download/win.
- Follow the installer prompts. The default options work fine.
- Close and reopen your terminal.
- Run git --version again and confirm you see a version number.
- Check your Azure Functions Core Tools version by running this command:
func --version
✔️ I see version 4.x or higher
Azure Functions Core Tools is ready. Move on to the accounts check below.
ⓧ I see an older version
You need version 4. Upgrade using the same method you originally installed with.
macOS
- Upgrade using Homebrew by running:
brew tap azure/functions
brew install azure-functions-core-tools@4
brew link --overwrite azure-functions-core-tools@4
Windows
- Download and run the latest v4.x 64-bit MSI from the official Core Tools install page.
- If you previously installed via MSI, uninstall the old version from Add or Remove Programs first.
- Run func --version again and confirm you see a 4.x version number.
ⓧ Command not found
Install Azure Functions Core Tools v4 for your operating system.
macOS
- Install using Homebrew by running these commands:
brew tap azure/functions
brew install azure-functions-core-tools@4
Windows
- Download and run the v4.x 64-bit MSI from the official Core Tools install page.
- Follow the installer prompts.
Linux
- Follow the Linux instructions on the official Core Tools install page to add the Microsoft package repository and install azure-functions-core-tools-4.
- Close and reopen your terminal.
- Run func --version again and confirm you see a 4.x version number.
What is Azure Functions Core Tools?
Azure Functions Core Tools is a command-line tool that lets you run and test Azure Functions on your own machine before deploying to the cloud. The func command starts a local runtime that mimics the Azure environment.
You also need an Azure account and a GitHub account.
- If you do not have an Azure account, create a free one at azure.microsoft.com/free.
- If you do not have a GitHub account, create one at github.com.
Create a GitHub repository and project structure
Your code needs a home on GitHub so that Azure Static Web Apps can connect to it later for automatic deployments.
- Go to github.com/new in your browser.
- Enter portfolio-api as the repository name.
- Select Public.
- Check the box for Add a README file.
- Click Create repository.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the repository by running this command (replace the URL with your own):
git clone [[REPO_URL="https://github.com/YOUR_USERNAME/portfolio-api.git"]]
- Move into the project folder and create the directory structure by running these commands:
cd portfolio-api
mkdir src api
- Create the placeholder files by running these commands:
touch src/index.html
touch staticwebapp.config.json
- Confirm the files and folders exist by running:
ls
You should see README.md, api, src, and staticwebapp.config.json in the output.
Don't see the expected files?
- Make sure you ran cd portfolio-api before running mkdir and touch. If you are not inside the cloned folder, the files land in the wrong place.
- On Windows, touch may not be available. Use New-Item src/index.html and New-Item staticwebapp.config.json in PowerShell instead.
Why can't I see my files?
Initialize the API as a Python Azure Functions project
The api/ folder is where your serverless backend lives. It needs a Python virtual environment, the azure-functions package, and a few configuration files before the Functions host can start.
- Move into the api folder and create a virtual environment by running these commands:
cd api
python3 -m venv .venv
- Activate the virtual environment by running:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\Activate
You should see (.venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
- Install the Azure Functions SDK by running:
pip install azure-functions
You should see output ending with a success message confirming the package was installed.
Seeing a pip error?
- Make sure your virtual environment is active. You should see (.venv) in your prompt. If not, run the activation command again.
- If pip is not found, try pip3 instead.
Help me fix my pip install error.
- Create the requirements.txt file to document the project dependency by running:
echo "azure-functions" > requirements.txt
- Confirm the file was created correctly by running:
cat requirements.txt
You should see azure-functions printed as the only line.
- Open your code editor. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter.
- Click File in the top menu bar.
- Click Open Folder.
- Select the portfolio-api folder on your Desktop, then click Open.
- Create a new file at api/host.json and paste the following content:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
What does host.json do?
This file configures the Azure Functions runtime. The version field tells the host to use version 2.0 of the configuration schema.
The extensionBundle section automatically pulls in the binding extensions your functions need, so you do not have to install them manually.
- Save api/host.json.
- Confirm the file exists by switching to your terminal and running:
ls
You should see .venv, host.json, and requirements.txt listed.
- Create a new file at api/function_app.py and paste the following content:
import azure.functions as func
# Create the FunctionApp instance that the host discovers
app = func.FunctionApp()
What does this code do?
This is the entry point for your Azure Functions project. import azure.functions as func pulls in the SDK.
The FunctionApp() instance is what the Functions host scans for route decorators. Right now it has no routes. You will add one in Step 3.
- Save api/function_app.py.
Before you run the verification, what do you think will happen when the Functions host starts with no routes defined?
- Verify the Functions host starts by running:
func start
The host starts and shows status messages, but it will not list any HTTP functions because no routes are defined yet.
That is the expected output at this stage. The important thing is that it starts without errors.
- Press Ctrl+C to stop the Functions host.
Seeing an error when running func start?
- Make sure your virtual environment is active. You should see (.venv) in your terminal prompt.
- Make sure you are inside the api/ folder, not the project root. Run pwd to check your current directory.
- Confirm host.json and function_app.py both exist in the api/ folder.
Help me fix my func start error.
✔️ Awesome, I've got everything!
Double check that you saved all files. Your project is ready for the next step.
ⓧ I'd like to double check the full code
Here is the content of each file you should have at this point. Compare them with yours.
api/function_app.py:
import azure.functions as func
app = func.FunctionApp()
api/host.json:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
api/requirements.txt:
azure-functions
src/index.html and staticwebapp.config.json should both be empty placeholder files at this point.
Your tools are installed, your repo is cloned, and the Functions host starts cleanly. Next up, you will build the portfolio frontend that calls your API.
Build the Static Frontend
Your tools are installed and your portfolio-api repository is cloned with all the placeholder files in place. Now it is time to give your portfolio a face.
A portfolio site needs a frontend that visitors actually see. In this step, you will build a styled HTML page that tries to fetch data from a serverless API endpoint. The API does not exist yet, so you will see firsthand what happens when a frontend calls a backend that is not there.
In this step, get ready to:
- Build a styled portfolio page with sections for your name, skills, and experience.
- Wire up JavaScript to fetch resume data from a serverless API route.
- Configure the staticwebapp.config.json file for Azure Static Web Apps.
Write the portfolio HTML page
Your src/index.html file is currently empty. You will fill it with three chunks: the document head with CSS styles, the page body with portfolio sections, and a JavaScript block that calls your API.
- Open src/index.html in your code editor.
- Replace the empty file with the HTML document head and CSS styles by pasting this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Portfolio</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 2rem; }
header { text-align: center; padding: 3rem 0; border-bottom: 2px solid #0078d4; margin-bottom: 2rem; }
header h1 { font-size: 2.5rem; color: #0078d4; }
header p { font-size: 1.2rem; color: #666; margin-top: 0.5rem; }
section { margin-bottom: 2rem; }
h2 { color: #0078d4; margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; }
.skills { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.skill-tag { background: #e8f4fd; color: #0078d4; padding: 0.3rem 0.8rem; border-radius: 20px; font-size: 0.9rem; }
.experience-item { margin-bottom: 1rem; padding: 1rem; background: #f9f9f9; border-radius: 8px; }
.experience-item h3 { color: #333; }
.experience-item p { color: #666; font-size: 0.9rem; }
#resume-loading { color: #999; font-style: italic; }
footer { text-align: center; padding: 2rem 0; color: #999; border-top: 1px solid #eee; margin-top: 2rem; }
</style>
</head>
What do these styles do?
The CSS resets default browser spacing, centers the page content at a max width of 800 pixels, and uses Azure's signature blue (#0078d4) as the accent color for headings and borders.
The .skill-tag class creates rounded pill-shaped badges for skills. The .experience-item class gives each job entry a light background card.
- Directly below the closing </head> tag, paste the page body with your portfolio sections:
<body>
<header>
<h1 id="header-name">My Portfolio</h1>
<p id="header-title">Loading...</p>
</header>
<section id="about">
<h2>About Me</h2>
<p id="about-summary">Loading resume data...</p>
</section>
<section id="skills-section">
<h2>Skills</h2>
<div id="skills" class="skills">
<span id="resume-loading">Loading...</span>
</div>
</section>
<section id="experience-section">
<h2>Experience</h2>
<div id="experience">
<span id="resume-loading">Loading...</span>
</div>
</section>
<footer>
<p>Powered by Azure Static Web Apps and Azure Functions</p>
</footer>
</body>
</html>
What does this HTML create?
The body has four visible sections: a centered header with your name and title, an About Me paragraph, a Skills container for tag badges, and an Experience container for job cards.
Every section shows placeholder text like "Loading..." right now. The JavaScript you add next will replace these placeholders with real data from the API.
- Save src/index.html.
- Open the file in your browser by double-clicking it in Finder (macOS) or File Explorer (Windows), or by dragging it into a browser window.
You should see a centered portfolio layout with "My Portfolio" as the main heading, blue accent lines under each section title, and "Loading..." in every content area.
Seeing a blank page?
- Make sure the </head> tag sits directly above the <body> tag with no gap or duplicate tags between them.
- Check that you pasted both code blocks into the same file and did not overwrite the first block with the second.
- Confirm the file is saved before refreshing the browser.
Still stuck? Why is my index.html showing a blank page in the browser?
- Now add the JavaScript that connects your page to the API. Find the </footer> tag in your code and paste the following script block between </footer> and </body>:
<script>
async function loadResume() {
try {
const response = await fetch('/api/resume');
if (!response.ok) throw new Error('API not available');
const data = await response.json();
document.getElementById('header-name').textContent = data.name;
document.getElementById('header-title').textContent = data.title;
document.getElementById('about-summary').textContent = data.summary;
const skillsContainer = document.getElementById('skills');
skillsContainer.innerHTML = data.skills
.map(skill => `<span class="skill-tag">${skill}</span>`)
.join('');
const experienceContainer = document.getElementById('experience');
experienceContainer.innerHTML = data.experience
.map(exp => `
<div class="experience-item">
<h3>${exp.role} at ${exp.company}</h3>
<p>${exp.duration}</p>
</div>
`)
.join('');
} catch (error) {
document.getElementById('about-summary').textContent =
'Could not load resume data. API endpoint not available.';
console.error('Failed to fetch resume:', error);
}
}
loadResume();
</script>
What does this script do?
- fetch('/api/resume') sends a GET request to the /api/resume endpoint and expects a JSON response back.
- If the request succeeds, the function reads the name, title, summary, skills, and experience fields from the JSON and injects them into the matching HTML elements.
- If the request fails (no server running, wrong URL, network error), the catch block replaces the About Me text with an error message so the page degrades gracefully.
- Save src/index.html.
✔️ Awesome, I've got everything!
Great. Confirm you have saved the file before moving on.
ⓧ I'd like to double check the full code
Your src/index.html should contain three sections in this order:
- The <head> block with all CSS styles (the first code block you pasted).
- The <body> with header, About Me, Skills, Experience sections, and a footer.
- The <script> block with the loadResume() function, placed between </footer> and </body>.
Scroll up and confirm each code block is present in your file, in order.
Add the staticwebapp.config.json configuration
Azure Static Web Apps reads a configuration file to know how to serve your site and which runtime to use for the API backend.
- Open staticwebapp.config.json in your code editor (it is in the root of your portfolio-api folder) and replace the empty file with this configuration:
{
"navigationFallback": {
"rewrite": "/index.html"
},
"platform": {
"apiRuntime": "python:3.11"
}
}
What does this configuration do?
- navigationFallback tells Azure to serve index.html for any URL that does not match a real file. This prevents 404 errors when visitors navigate directly to a path on your site.
- apiRuntime set to python:3.11 tells the platform to use Python 3.11 for the managed Azure Functions backend. Azure reads this value during deployment to set up the correct runtime for your API code.
- Save staticwebapp.config.json.
✔️ Awesome, I've got everything!
Great. Azure will use this file when you deploy in Step 4.
ⓧ I'd like to double check the full code
{
"navigationFallback": {
"rewrite": "/index.html"
},
"platform": {
"apiRuntime": "python:3.11"
}
}
Open and test the static page locally
Your frontend is complete and the config is set. Before you move on, take a moment to predict what will happen when the JavaScript runs.
- Think about this: when the page loads, loadResume() fires a request to /api/resume. Do you think that request will succeed or fail right now? What will the page show?
- Switch back to your browser where you have src/index.html open and refresh the page.
The About Me section now reads "Could not load resume data. API endpoint not available." instead of the earlier "Loading resume data..." placeholder. The Skills and Experience sections still show "Loading..." because the catch block only updates the summary text.
This is the designed gap. You have a complete frontend that knows how to display resume data, but there is no /api/resume endpoint to respond to the request. The next step closes that gap by building the Python Azure Functions backend.
Still seeing the old "Loading resume data..." text?
- Make sure you saved src/index.html after adding the <script> block.
- Do a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear the browser cache.
- Open the browser console with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) and check for error messages. You should see a Failed to fetch error, which confirms the script is running.
Need more help? Why is my page not showing the error message after I added the script?
Your frontend is built and waiting for data. Next up, you will create the Python serverless function that returns your resume as JSON, giving the page the data it needs to come alive.
Write the Serverless Resume API
Your portfolio page is styled and laid out, but the Resume section is stuck on "Loading..." because the frontend's fetch('/api/resume') call has nowhere to go. The endpoint simply does not exist yet.
In this step, you will write the Python Azure Function that powers that endpoint and test the full stack locally. By the end, your page will display real resume data pulled from your own serverless API.
In this step, get ready to:
- Write a Python Azure Function that returns your resume as structured JSON.
- Test the frontend and API together locally using the SWA CLI.
Write the /api/resume function
The Python v2 programming model for Azure Functions lets you define HTTP endpoints with decorators directly in your code. Instead of managing separate configuration files, you decorate a plain Python function and it becomes a live API route.
- In api/function_app.py, find the line that says import azure.functions as func and add import json directly below it.
- Then find app = func.FunctionApp() and copy this function below it:
# Decorator registers this function at /api/resume
@app.route(route="resume", methods=["GET"])
def resume(req: func.HttpRequest) -> func.HttpResponse:
# Structure your resume data as a dictionary
resume_data = {
"name": "Your Name",
"title": "Software Developer",
"summary": "A developer who builds serverless applications on Azure.",
"skills": ["Python", "Azure Functions", "HTML/CSS", "JavaScript", "Git"],
"experience": [
{
"role": "Software Developer",
"company": "Tech Company",
"duration": "2023 - Present"
},
{
"role": "Junior Developer",
"company": "Startup Inc",
"duration": "2021 - 2023"
}
]
}
# Convert the dictionary to JSON and return it
return func.HttpResponse(
body=json.dumps(resume_data),
mimetype="application/json",
status_code=200
)
What does this code do?
The @app.route decorator tells Azure Functions to expose this function as an HTTP endpoint at /api/resume. The /api prefix is added automatically by Azure Static Web Apps.
Inside the function, resume_data is a Python dictionary holding your name, title, skills, and experience. json.dumps() converts that dictionary into a JSON string, which the HttpResponse sends back to whoever calls the endpoint.
- Save api/function_app.py.
Personalise your resume data
Replace the placeholder values in resume_data with your own name, title, skills, and experience. This data will appear on your live portfolio page.
✔️ Awesome, I've got everything!
Great. Make sure you have saved api/function_app.py before moving on.
ⓧ I'd like to double check the full code
Your api/function_app.py file has three sections. Scroll through and confirm each one is present, in order:
- Two import lines at the top: import azure.functions as func and import json.
- The FunctionApp instance: app = func.FunctionApp().
- The resume function with the @app.route(route="resume", methods=["GET"]) decorator, a resume_data dictionary, and a return func.HttpResponse(...) at the end.
Test the full stack locally with the SWA CLI
Right now, you can open index.html in a browser, but the fetch('/api/resume') call still fails because no local server is handling the /api route. The Azure Static Web Apps CLI (SWA CLI) solves this by running the static frontend and the Functions API together on a single local port.
The SWA CLI requires Node.js and npm to install. Check if you already have Node.js by running this command:
node --version
✔️ I see a version number
Node.js is ready. Move on to the SWA CLI install below.
ⓧ Command not found
- Download and install Node.js from https://nodejs.org. Choose the LTS version.
- Follow the installer prompts and accept the defaults.
- Close and reopen your terminal, then run node --version again to confirm it works.
- Install the SWA CLI globally by running this command:
npm install -g @azure/static-web-apps-cli
What is the SWA CLI?
The SWA CLI emulates the Azure Static Web Apps environment on your machine. It serves your static files and proxies /api requests to the Functions host, exactly like the real Azure service does in production.
Seeing a permission error?
On macOS or Linux, you may need to prefix the install command with sudo. On Windows, try running your terminal as Administrator.
help me fix a permission error when installing an npm package globally
Before starting the SWA CLI, you need to activate the Python virtual environment you created in Step 1. The SWA CLI runs func start internally, and it needs access to the azure-functions package inside your venv.
- Make sure you are in the portfolio-api project folder.
- Activate the virtual environment by running the command for your operating system:
macOS
source api/.venv/bin/activate
This command also works on Linux.
Windows
api\.venv\Scripts\Activate
- Start the local development server by running this command from the project root:
swa start src --api-location api
What does this command do?
The swa start command takes two arguments here. src tells the CLI where your static HTML files live. --api-location api tells it where your Functions project lives.
The CLI starts a local server at http://localhost:4280 that serves both together. Any request to /api/* is routed to the Functions host automatically.
Before you open the browser, what do you think the Resume section will show this time?
- Open http://localhost:4280 in your browser.
The "Loading..." message is gone. Your portfolio page now displays the name, title, skills, and experience pulled from your /api/resume endpoint.
- Open http://localhost:4280/api/resume directly in a new browser tab to see the raw JSON response.
Still seeing Loading or an error?
- Check the terminal where swa start is running. Look for error messages about the API failing to start.
- Make sure you activated the virtual environment before running swa start. Without it, the Functions host cannot find the azure-functions package.
- Confirm that api/function_app.py has import json at the top. A missing import causes a startup error.
- Make sure you are running swa start from the portfolio-api project root, not from inside the api/ folder.
help me debug why my SWA CLI local server is not loading my resume data
Your frontend and API are working together locally. Next up, you will deploy the entire stack to Azure so anyone on the internet can visit your portfolio.
Deploy to Azure Static Web Apps
In the previous step, you built the serverless resume API and saw your full portfolio working locally.
Your portfolio works on your machine, but nobody else can visit it. This step puts it live on the internet with a real URL and sets up automatic deployments from GitHub.
Every time you push code to GitHub, a GitHub Actions workflow will automatically rebuild and redeploy your site. Zero manual steps after this initial setup.
In this step, get ready to:
- Push your project code to GitHub.
- Create an Azure Static Web App connected to your repository.
- Verify your live site at a public URL.
Push your code to GitHub
Before Azure can deploy your site, all your files need to be in your GitHub repository. Right now the code is only on your local machine.
- Stage all your project files by running this command from the portfolio-api folder:
git add .
- Commit your changes by running:
git commit -m "Initial portfolio with resume API"
- Push your code to GitHub by running:
git push origin main
- Confirm the push succeeded by looking at the terminal output. You should see a line that ends with main -> main.
Push failed or asked for credentials?
If git asks for a username and password, GitHub no longer accepts passwords for HTTPS. You need a personal access token or SSH key instead.
If you see a permissions error, make sure you are the owner of the portfolio-api repository on GitHub.
help me fix a git push authentication error
- Open your portfolio-api repository on GitHub in your browser.
- Confirm you can see src/index.html, api/function_app.py, and staticwebapp.config.json in the file list.
Create the Static Web App in the Azure Portal
Azure Static Web Apps is a hosting service that serves your static files and your serverless API from a single URL. When you connect it to GitHub, it auto-generates a CI/CD workflow that deploys every push.
- Open the Azure Portal at https://portal.azure.com.
- Select Create a Resource.
- Search for Static Web Apps in the search box.
- Select Static Web Apps from the results.
- Select Create.
You are now on the Basics tab. Fill in the resource details first.
- Select your Azure subscription from the Subscription dropdown.
- Under Resource Group, select Create new and enter portfolio-api-rg.
- In the Name field, enter a name for your Static Web App (for example, portfolio-api).
- Set Plan type to Free.
- Select a region closest to you.
- Under Source, select GitHub.
Why the Free plan?
The Azure Static Web Apps Free tier includes 100 GB of bandwidth per month, free SSL certificates, 2 custom domains, and 1 million free Azure Functions executions. For a portfolio site, this is more than enough.
Azure now asks you to connect your GitHub account.
- Select Sign in with GitHub and authorize Azure when prompted.
- Select your GitHub organization from the Organization dropdown.
- Select portfolio-api from the Repository dropdown.
- Select main from the Branch dropdown.
Don't see your repository?
You may need to authorize Azure Static Web Apps in GitHub. Go to your GitHub profile, then Settings, then Applications, then Authorized OAuth Apps. Select Azure Static Web Apps and select Grant.
help me connect my GitHub repo to Azure Static Web Apps
Now scroll down to the Build Details section. This tells Azure where your frontend and API code live.
- Select Custom from the Build Presets dropdown.
- Set App location to src.
- Set Api location to api.
- Clear the Output location field so it is empty.
- Select Review + create.
- Select Create.
What do App location and Api location mean?
App location tells Azure which folder contains your static frontend files. You set it to src because that is where index.html lives.
Api location tells Azure which folder contains your serverless functions. You set it to api because that is where function_app.py lives. Azure automatically serves these under the /api route.
- Wait for the deployment to complete, then select Go to resource.
You are now on the Overview page for your new Static Web App. Azure has automatically committed a GitHub Actions workflow file to your repository.
What just happened behind the scenes?
When you clicked Create, Azure did two things. First, it provisioned the hosting infrastructure for your site. Second, it committed a workflow file to your GitHub repository at .github/workflows/ that uses the Azure/static-web-apps-deploy@v1 action.
This workflow triggers on every push to main, building your frontend from app_location and deploying your API from api_location. You never have to deploy manually again.
Watch the deployment and verify the live site
The GitHub Actions workflow is already running because Azure just pushed the workflow file to your repository. It takes about 1-2 minutes to build and deploy.
- Open your portfolio-api repository on GitHub.
- Select the Actions tab.
- You should see a workflow run called Azure Static Web Apps CI/CD in progress or completed. Wait for it to show a green checkmark.
Workflow failed with a red X?
Click on the failed workflow run to see the logs. Common causes include the App location or Api location being set incorrectly when you created the resource. Double check that App location is src and Api location is api.
help me debug a failed Azure Static Web Apps GitHub Actions deployment
Once the workflow shows a green checkmark, your site is live.
- Go back to the Azure Portal.
- On your Static Web App's Overview page, find the URL field. It looks like https://<random-name>.azurestaticapps.net.
Before you click that URL, what do you think you will see? Will the Resume section show your data, or will it still say "Loading..."?
- Click the URL to open your live site in a new browser tab.
Your portfolio page loads with your name, title, skills, and experience pulled from the /api/resume endpoint. The "Loading..." placeholder is gone.
- Now test the API directly. In your browser's address bar, add /api/resume to the end of your live URL and press Enter.
You should see the raw JSON response containing your name, title, summary, skills, and experience. This is the same data your frontend fetches, now live on the internet for anyone to access.
What is that URL?
Azure Static Web Apps gives every app a free *.azurestaticapps.net URL with automatic SSL. You can share this URL directly. Anyone who visits it will see your portfolio and can hit your API endpoint.
- Finally, pull the auto-generated workflow file to your local repository by running:
git pull
Why do I need to pull?
Azure committed the GitHub Actions workflow file directly to your repository on GitHub. Your local copy does not have it yet. Running git pull synchronizes your local repo so future pushes include the workflow file.
Your portfolio is live on the internet with a real URL and automatic deployments. Every time you push to main, GitHub Actions rebuilds and redeploys your site automatically. Next up, you will extend the API with a second endpoint in the Secret Mission.
Secret mission
Add a Projects API Endpoint
Your portfolio site has a working resume API, but a real portfolio showcases your projects too. Can you add a second /api/projects endpoint that returns a JSON array of your projects, then wire the frontend to display them as cards? Follow the same pattern you used for /api/resume and prove you can extend the architecture on your own.
Clean Up Your Resources
Clean Up Your Resources
This project uses the Azure Static Web Apps Free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Azure Static Web App (Free tier, no cost).
- GitHub repository (portfolio-api) with an auto-generated GitHub Actions workflow.
- Local portfolio-api project folder on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building on your portfolio site.
- Your site stays live at its *.azurestaticapps.net URL indefinitely. The Free tier costs nothing.
- Every time you push code to your portfolio-api repository, GitHub Actions automatically redeploys your site.
- Locally installed tools (Python, Git, Azure Functions Core Tools, SWA CLI) remain on your machine for future projects.
Pause - I'll come back to this later
Free up local resources while keeping the live site available. Since the Free tier has no cost, there is nothing to pause on the Azure side.
- If you still have the SWA CLI dev server running locally, stop it by pressing Ctrl+C in the terminal.
- If your Python virtual environment is active, deactivate it by running:
deactivate
- Your live site remains accessible at its *.azurestaticapps.net URL. You can resume working by navigating back to your portfolio-api folder and pushing new changes.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below.
Delete the Azure Static Web App:
- Open the Azure Portal.
- Search for your Static Web App name in the top search bar and select it.
- Click Delete.
- Click Yes to confirm. This removes the site and its managed Functions backend.
Delete the GitHub repository:
- Open your portfolio-api repository on GitHub.
- Click Settings in the repository tab bar.
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository.
- Click I have read and understand these effects.
- Type the repository name to confirm, then click Delete this repository.
Delete the local project folder:
- Delete the portfolio-api folder from your Desktop by running:
macOS
rm -rf ~/Desktop/portfolio-api
Windows
Remove-Item -Recurse -Force "$env:USERPROFILE\Desktop\portfolio-api"
Nice Work!
Nice Work!
You just built and deployed a live portfolio site with a serverless Python API backend, straight from GitHub to a real URL on the internet!
You've learned how to:
- Build a static HTML/CSS portfolio page that fetches data from a serverless API using JavaScript fetch calls.
- Create a Python Azure Function using the v2 decorator-based programming model that returns structured JSON at /api/resume.
- Deploy the full stack to Azure Static Web Apps on the Free tier with automatic CI/CD via GitHub Actions, giving you a live, shareable *.azurestaticapps.net URL with free SSL.
- Secret Mission: Extended the API with a /api/projects endpoint and wired the frontend to display project data dynamically.
Ready to quiz yourself?