Deploy a Serverless Contact Endpoint
Build a secured HTTP endpoint on Azure Functions with live monitoring.
Introduction
30 Second Summary
Every website with a contact form needs somewhere to send those messages. Traditionally that means setting up a server, keeping it running 24/7, and paying for it even when nobody is filling out the form.
In this project, you will deploy a serverless HTTP endpoint on Azure Functions that accepts contact-form submissions, secure it with access keys, and watch live traffic flow through Application Insights.
What You'll Build
You will send a POST request from the terminal, see a structured JSON response come back instantly, then watch that request light up in a real-time monitoring dashboard.
By the end of this project, you'll have:
- A live serverless endpoint you can call from anywhere. Send it a name, email, and message and get a timestamped JSON response back in milliseconds.
- A key-secured API that rejects unauthorized callers with a 401. Only clients with your function key can reach the endpoint.
- A real-time monitoring dashboard where you can fire off requests and watch them appear in Live Metrics within seconds, complete with response times and success rates.
- Secret Mission: Add input validation that rejects malformed requests with descriptive error messages.
Are there any prerequisites?
You need an Azure account with an active subscription. All work happens in Azure Cloud Shell through your browser, so there is nothing to install locally.
Before We Start
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 deploy a live HTTP endpoint that handles contact-form submissions, secure it so only authorized clients can reach it, and watch real-time traffic flow through monitoring. All without provisioning or managing a single server.
Set Up Cloud Shell and Create Azure Resources
Before you can deploy a serverless function, you need a few foundational resources in Azure. A resource group organizes everything you create, and a storage account gives the Function App somewhere to store its code.
Cloud Shell gives you a fully configured terminal right in your browser. It comes with the Azure CLI and Azure Functions Core Tools pre-installed, so you can skip local setup entirely.
In this step, get ready to:
- Open Azure Cloud Shell in the Azure Portal.
- Create a resource group and storage account for your Function App.
- Confirm Azure Functions Core Tools is available.
Open Azure Cloud Shell
Cloud Shell is a browser-based terminal built into the Azure Portal. It has every tool you need for this project pre-installed.
- Sign in to the Azure Portal if you haven't already.
- In the top navigation bar, click the Cloud Shell icon (it looks like a terminal prompt: >_).
- If prompted to choose a shell, select Bash.
- If this is your first time using Cloud Shell, it asks you to create a storage account to persist your session files. Follow the prompts to let it provision one automatically.
After a few seconds, a terminal panel appears at the bottom of the Azure Portal with a Bash prompt.
Cloud Shell not loading?
Make sure you have an active Azure subscription. Cloud Shell requires a subscription to provision its backing storage.
If you see a spinning icon for more than 30 seconds, try refreshing the browser and clicking the Cloud Shell icon again.
Help me troubleshoot Azure Cloud Shell not loading.
Create a resource group and storage account
A resource group acts as a folder for all the Azure resources in this project. The storage account gives your Function App a place to store its deployment packages.
- Create a resource group called ContactFuncRG by running this command in Cloud Shell:
az group create --name ContactFuncRG --location eastus
You see a JSON response with "provisioningState": "Succeeded" confirming the resource group was created.
- Create a storage account by running this command (replace the storage name with your own globally unique name):
az storage account create --name [[STORAGE_NAME="your unique name e.g. contactfuncstorage123"]] --location eastus --resource-group ContactFuncRG --sku Standard_LRS
What makes a valid storage account name?
Storage account names must be globally unique across all of Azure. They can only contain lowercase letters and numbers, and must be between 3 and 24 characters long.
Try something like your initials plus the word "contactfunc" and a few digits. For example: jdcontactfunc42.
This command takes about 20 seconds. Once it finishes, you see another JSON response with "provisioningState": "Succeeded".
Seeing a storage name error?
If you see a message about the storage account name already being taken, choose a different name and run the command again. Remember to use only lowercase letters and numbers.
Help me fix my storage account creation error.
Before you move on, predict what the following command will print. Do you think it will show your resource group name, or something else?
- Verify your resource group exists by running:
az group list --query "[?name=='ContactFuncRG'].name" -o tsv
You see ContactFuncRG printed on its own line. That confirms your resource group is ready.
Confirm Azure Functions Core Tools is installed
Cloud Shell comes with Azure Functions Core Tools pre-installed. This is the CLI you use to scaffold, test, and deploy functions.
- Check the installed version by running:
func --version
✔️ I see version 4.x
You see a version number starting with 4 (for example, 4.0.6610). Azure Functions Core Tools is ready to go.
ⓧ I see an error or command not found
This is unusual since Cloud Shell includes Core Tools by default. Try these steps:
- Make sure you selected Bash as your shell type, not PowerShell. You can switch by clicking the shell-type dropdown at the top left of the Cloud Shell panel.
- Close Cloud Shell and reopen it by clicking the >_ icon again.
- If the issue persists, your Cloud Shell session may need to be reset. Click the gear icon in the Cloud Shell toolbar and select Reset user settings.
Help me get Azure Functions Core Tools working in Cloud Shell.
Your Azure environment is all set up. Next, you create the Function App itself and get a live URL running on the Flex Consumption plan.
Create a Function App on the Flex Consumption Plan
Your Azure resources are in place. Cloud Shell is connected, and your resource group and storage account are ready to go.
Now you need somewhere to actually run your code. A Function App is the serverless container that hosts your HTTP endpoints on Azure. You'll create one on the Flex Consumption plan, which costs nothing when idle and scales automatically when traffic arrives.
In this step, get ready to:
- Create a Function App on the Flex Consumption plan from Cloud Shell.
- Confirm the Function App, Storage Account, and Application Insights are all visible in the Azure Portal.
Create the Function App from Cloud Shell
The Function App is the serverless host for your contact endpoint. One CLI command provisions it, connects your storage account, and sets the Node.js runtime.
- In Cloud Shell, create the Function App by running this command:
az functionapp create --resource-group ContactFuncRG --name [[APP_NAME="your globally unique app name"]] --storage-account [[STORAGE_NAME="your storage account name from Step 1"]] --flexconsumption-location eastus --runtime node --runtime-version 20
What does this command do?
- --resource-group ContactFuncRG places the app in the resource group you created in Step 1.
- --name sets a globally unique name for the Function App. This becomes part of the public URL.
- --storage-account connects the storage account you already created.
- --flexconsumption-location eastus creates the app on the Flex Consumption plan in East US. This flag is what selects the Flex Consumption plan specifically.
- --runtime node --runtime-version 20 sets the runtime to Node.js 20.
- The command also automatically creates an Application Insights instance for monitoring. You don't need a separate setup step for it.
- Wait for the command to finish. It takes around 30-60 seconds. You'll see a large JSON output printed when provisioning completes.
Why Flex Consumption over classic Consumption?
Flex Consumption is the current recommended serverless plan for new function apps. It offers faster cold starts, VNet support, and per-function scaling up to 1,000 instances.
For this project, the key benefit is cost. The free grant includes 250,000 executions and 100,000 GB-s per month. Your testing won't come close to that limit.
Seeing a naming error?
- Function App names must be globally unique across all of Azure. If you see a conflict error, try a more specific name like contact-func-yourname-2026.
- Make sure your --storage-account value exactly matches the storage account name you created in Step 1. A typo here causes a 'not found' error.
- If you see an error about an unsupported region, double check that you typed --flexconsumption-location eastus (not --location).
Why is my az functionapp create command failing?
Confirm your resources in the Azure Portal
The CLI gave you JSON output, but let's verify everything landed correctly in the Portal where you can see all three resources together.
- In the Azure Portal, click Resource groups in the left sidebar (or search for it in the top search bar).
- Click ContactFuncRG to open your resource group.
- You should see three resources listed: your Function App, your Storage Account, and an Application Insights resource.
Where did Application Insights come from?
Azure created it automatically when you ran the az functionapp create command. It's already connected to your Function App and ready to receive telemetry. You'll use it in a later step to watch live traffic.
Before you run the next command, what state do you think your Function App is in right now?
- Verify the Function App is running by running this command in Cloud Shell:
az functionapp show --name [[APP_NAME="your globally unique app name"]] --resource-group ContactFuncRG --query "state" -o tsv
You should see Running printed. Your Function App is live and waiting for code.
Not seeing Running?
- If you see Stopped, wait 30 seconds and run the command again. Flex Consumption apps can take a moment to finish starting up.
- If you see an error about the resource not being found, check that your --name value matches the name you used in the create command.
My Function App isn't showing as Running in Azure
Your Function App is running and ready. Next up, you'll deploy actual code to it and hit a live endpoint from your browser.
Deploy an HTTP-Triggered Contact Endpoint
Your Function App is running on the Flex Consumption plan, but right now it is an empty shell with no code behind it.
Without a deployed function, hitting the app's URL returns nothing useful. In this step, you scaffold a Node.js project, write the contact endpoint code, and publish it so you have a live URL you can call from anywhere.
In this step, get ready to:
- Scaffold a new Azure Functions project with an HTTP trigger.
- Write a contact endpoint that accepts POST requests and returns structured JSON.
- Deploy to Azure and test the live endpoint with curl.
Scaffold the Functions project and HTTP trigger
Azure Functions Core Tools lets you create a local project structure, then deploy it to your Function App in the cloud.
- Initialize a new Functions project by running this command in Cloud Shell:
func init ContactFunc --worker-runtime node --language javascript
- Move into the new project folder by running:
cd ContactFunc
- Create an HTTP-triggered function named contact by running:
func new --name contact --template "HTTP trigger" --authlevel "anonymous"
What do these commands do?
`func init` creates the project scaffold with a `package.json`, `host.json`, and the folder structure for Node.js v4 functions.
`func new` generates a starter function file at `src/functions/contact.js` using the v4 programmatic model. Setting `--authlevel "anonymous"` means anyone can call the endpoint without a key for now.
- Confirm the project was created by running:
ls src/functions/
You should see contact.js listed in the output.
Don't see contact.js?
Make sure you ran cd ContactFunc before running func new. The func new command must run inside the project directory.
If you see a src/functions directory but no file inside, try running the func new command again from the ContactFunc folder.
help me troubleshoot why my function file wasn't created
Write the contact endpoint code
The scaffold generates a simple starter template. You need to replace it with your contact form handler that processes POST requests and returns structured JSON.
- Open the generated file in the Cloud Shell editor by running:
code src/functions/contact.js
- Select all the existing code and delete it.
- Paste in the following code that registers the function and handles GET requests:
const { app } = require('@azure/functions');
app.http('contact', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
context.log('Contact function processed a request.');
if (request.method === 'GET') {
return {
jsonBody: { status: 'ok', message: 'Contact endpoint is running. Send a POST with name, email, and message.' }
};
}
What does this code do?
`app.http('contact', {...})` registers an HTTP-triggered function named contact that responds to both GET and POST requests. The authLevel: 'anonymous' setting means no access key is required to call it.
The GET handler returns a simple status message so you can quickly verify the endpoint is alive from a browser.
- Directly below the GET handler block (still inside the handler function), paste the POST handling logic:
const body = await request.json();
const { name, email, message } = body;
context.log(`Contact form submission from: ${name} (${email})`);
return {
jsonBody: {
success: true,
received: {
name: name || 'not provided',
email: email || 'not provided',
message: message || 'not provided'
},
timestamp: new Date().toISOString()
}
};
}
});
What does this code do?
`await request.json()` parses the incoming POST body. The code destructures name, email, and message from it.
The response bundles those values into a structured JSON object with a timestamp so you can confirm exactly when the submission was received. Fields default to 'not provided' if the caller omits them.
- Save the file by pressing Ctrl+S in the Cloud Shell editor.
- Close the editor by pressing Ctrl+Q to return to the terminal.
✔️ Awesome, I've got everything!
Great. Double check that you saved the file before closing the editor.
ⓧ I'd like to double check the full code
Your src/functions/contact.js should contain two sections in order:
- The first chunk: the import, app.http registration with methods/authLevel/handler, the log line, and the GET return block.
- The second chunk: parsing the POST body, destructuring name/email/message, logging the submission, and returning the structured JSON response with timestamp. The handler and app.http call close with }); at the end.
Scroll up and confirm both chunks are present in order. The total file should be about 32 lines.
Deploy and test the live endpoint
Your code is written locally in Cloud Shell. Now you publish it to your Function App so it becomes a live HTTP endpoint on the internet.
- Deploy the project to your Function App by running:
func azure functionapp publish [[APP_NAME="your Function App name"]]
What does this command do?
This packages your local project and uploads it to the Function App running in Azure. Once complete, it prints the invoke URL for each function it deployed.
Deployment typically takes 30-60 seconds. You will see output confirming the upload and listing the function URL.
The output shows your function's live URL. Before you test it, what do you think will happen when you send a POST request with a name, email, and message?
- Test the endpoint by sending a POST request with curl:
curl -X POST https://[[APP_NAME="your Function App name"]].azurewebsites.net/api/contact -H "Content-Type: application/json" -d '{"name":"Test User","email":"test@example.com","message":"Hello"}'
You should see a JSON response containing "success": true along with the name, email, and message you submitted, plus a timestamp.
What should I see?
The response looks something like this:
{
"success": true,
"received": {
"name": "Test User",
"email": "test@example.com",
"message": "Hello"
},
"timestamp": "2026-07-30T12:00:00.000Z"
}
- Also verify the GET endpoint works by running:
curl https://[[APP_NAME="your Function App name"]].azurewebsites.net/api/contact
You should see a JSON response with "status": "ok" and a message telling you to send a POST.
Seeing a 404 or no response?
Make sure the deploy completed without errors. Scroll up in your Cloud Shell output and look for a line showing the function URL. If deployment failed, run the publish command again.
Check that you replaced the app name placeholder with the actual Function App name you created in the previous step. The URL must match your app exactly.
help me troubleshoot why my Azure Function endpoint is not responding
Your contact endpoint is live and accepting requests from anyone on the internet. Next up, you will lock it down with function keys so only authorized clients can reach it.
Secure the Endpoint with Function Keys
Your contact endpoint is live and working. But right now, anyone on the internet can call it without providing any credentials.
That means bots, scrapers, and random traffic can flood it with requests. In this step, you'll require a function key so only authorized clients can reach your endpoint.
In this step, get ready to:
- Change the authorization level from anonymous to function-level.
- Test that unauthorized requests are rejected with a 401.
- Retrieve the function key and make a successful authenticated request.
Change the authorization level and redeploy
Azure Functions supports three authorization levels for HTTP triggers: anonymous (no key required), function (function-specific key required), and admin (master key required).
- In Cloud Shell, make sure you're inside the ContactFunc directory.
- Open src/functions/contact.js in the Cloud Shell editor. Find this line:
authLevel: 'anonymous',
- Replace 'anonymous' with 'function', so the line now looks like this:
authLevel: 'function',
- Save contact.js.
- Redeploy the updated function to Azure by running this command:
func azure functionapp publish [[APP_NAME="your function app name"]]
What does this change do?
By setting authLevel: 'function', Azure Functions now checks every incoming request for a valid function key. Requests without the key get rejected before your handler code even runs.
Deployment failing?
Make sure you're still inside the ContactFunc directory where your project lives. Run ls src/functions/contact.js to confirm the file exists in your current location.
Help me troubleshoot my deployment error
Test the locked endpoint
You just told Azure to require a function key on every request to the contact endpoint. Before you test, predict what should happen when you call it without providing any key.
- Test the endpoint without a key by running:
curl -i https://[[APP_NAME="your function app name"]].azurewebsites.net/api/contact
You'll see HTTP 401 Unauthorized in the response. The endpoint is now locked down. Anyone calling without a valid key gets rejected immediately.
What is a 401 response?
HTTP 401 means the server understood the request but refuses to process it because the caller didn't provide valid credentials. In this case, the missing credential is the function key.
Still getting a 200 OK?
The deployment may not have finished propagating. Wait 30 seconds and try the curl command again. Also confirm you saved the file with authLevel: 'function' (not authLevel: 'anonymous') before redeploying.
Help me figure out why I'm still getting 200 OK
Retrieve the function key and test with it
Now that the endpoint rejects unauthenticated requests, you need the function key to make authorized calls. Azure generated this key automatically when the function was created.
- Retrieve your function's key by running:
func azure functionapp list-functions [[APP_NAME="your function app name"]] --show-keys
What does this command do?
This lists all functions deployed to your Function App along with their invoke URLs. The --show-keys flag appends the function key to each URL so you can copy it directly.
- In the output, find the invoke URL for the contact function. Copy the key value that appears after ?code= in the URL.
- Test the endpoint with your function key by running this command (replace your-function-key with the key you just copied):
curl -X POST "https://[[APP_NAME="your function app name"]].azurewebsites.net/api/contact?code=your-function-key" -H "Content-Type: application/json" -d '{"name":"Secure User","email":"secure@example.com","message":"This is authenticated"}'
You'll receive a JSON response with success: true and the submitted data echoed back. The function key authenticated your request successfully.
Getting 401 even with the key?
Double-check that you copied the entire key string without any extra spaces. The key is appended after ?code= with no spaces around the = sign. Also make sure the URL is wrapped in quotes so your shell doesn't split it at the ? character.
Help me debug my function key issue
✔️ Awesome, I've got everything!
Great. Double-check that you've saved your updated contact.js with authLevel: 'function' and confirmed both the 401 (no key) and the successful response (with key).
ⓧ I'd like to double check the full code
Your src/functions/contact.js has only one change from the previous step. Confirm line 5 reads:
authLevel: 'function',
Everything else in the file stays the same. The methods array, the handler function, and all the response logic remain unchanged from Step 3.
Your endpoint is locked down. Only clients with the correct function key can reach it. Next up, you'll connect Application Insights and watch live request telemetry flow through as traffic hits your function.
Monitor with Application Insights
Your contact endpoint is deployed and secured with a function key. But if a request fails or response times spike, you have no way to know.
Application Insights was created automatically with your Function App and captures request telemetry in real time. In this step, you'll open Live Metrics and watch traffic flow through your endpoint as you send requests from Cloud Shell.
In this step, get ready to:
- Open the Application Insights resource connected to your Function App.
- View Live Metrics for real-time request telemetry.
- Send traffic from Cloud Shell and confirm monitoring works.
Navigate to Live Metrics
Live Metrics displays telemetry within one second of a request arriving. It only streams data while the pane is open, and there is no charge for it.
- In the Azure Portal, navigate to your Function App by selecting Resource groups in the left sidebar, then clicking ContactFuncRG, then clicking your Function App name.
- On the Function App Overview page, scroll down to the Configured features section.
- Click the Application Insights link to open the connected Application Insights resource.
- In the Application Insights resource, look at the left menu under the Investigate section.
- Click Live Metrics.
- Keep this browser tab open. You will switch back to it after generating traffic.
What is Live Metrics?
Live Metrics streams request counts, response times, and failure rates to your browser in real time. Unlike standard Application Insights logs (which have up to 5 minutes of latency), Live Metrics shows data within one second.
Data only persists while the pane is open. Once you navigate away, the stream stops and the displayed data is discarded.
Generate traffic from Cloud Shell
With Live Metrics open in one tab, you'll now send several requests from Cloud Shell so you can see them appear in real time.
- Switch back to your Cloud Shell tab in the Azure Portal.
Before you run the next command, what do you think will happen in the Live Metrics panel? Will you see requests appear instantly, or after a delay?
- Send five POST requests to your secured endpoint by running this command:
for i in $(seq 1 5); do curl -s -X POST "https://[[APP_NAME="your-function-app-name"]].azurewebsites.net/api/contact?code=your-function-key" -H "Content-Type: application/json" -d "{\"name\":\"User $i\",\"email\":\"user$i@test.com\",\"message\":\"Message $i\"}"; echo; done
What does this command do?
This loop sends five separate POST requests to your contact endpoint, each with a unique name, email, and message. The -s flag silences progress output, and echo adds a newline between each JSON response so they're easy to read.
You should see five JSON responses in your terminal, each containing a success: true field and the submitted contact details.
- Switch back to your Live Metrics browser tab.
You should see incoming requests, response times, and a 100% success rate reflected in the Live Metrics charts. The data appears within seconds of your curl requests completing.
What should I see in Live Metrics?
The Incoming Requests chart shows a spike of 5 requests. The Request Duration chart shows how long each request took to process. The Sample Telemetry section at the bottom lists individual request details including the URL path and response code.
Not seeing any data in Live Metrics?
Make sure the Live Metrics pane was already open before you ran the curl loop. Data only streams while the pane is active.
If Live Metrics shows a connection message, wait 10-15 seconds for the initial connection to establish, then resend the requests.
Confirm your curl command included the correct function key. If you received 401 responses in the terminal, the requests were rejected and won't appear as successful in Live Metrics.
Help me troubleshoot why Live Metrics isn't showing my requests.
Secret mission
Add Input Validation
Your endpoint accepts any POST body without question. What happens when someone sends a request missing required fields, or with an invalid email format? Add validation logic that returns descriptive 400 Bad Request errors so malformed submissions get rejected before they reach your processing logic.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The Flex Consumption plan scales to zero when idle, so there are no ongoing compute costs while your function is not receiving traffic.
Why no cost warning?
The Flex Consumption plan includes 250,000 free executions and 100,000 GB-s per month. Your testing during this project uses a negligible fraction of that allowance. The Storage Account and Application Insights resource also stay within their free tiers at this scale.
Resources you used:
- Resource group: ContactFuncRG (contains all other resources).
- Storage account: your chosen name (stores function deployment packages).
- Function App on Flex Consumption plan: your chosen name (hosts the contact endpoint).
- Application Insights: created automatically with the Function App (collects telemetry).
Keep everything running
No action needed. Choose this if you plan to keep building on this function or want to reference the code later.
- The Flex Consumption plan scales to zero when idle. You are not charged for compute time when no requests arrive.
- The Storage Account and Application Insights resource remain in the free tier at this project's scale.
- Your function stays reachable at its URL with the function key you configured.
Pause - I'll come back to this later
Stop the Function App so it cannot receive any traffic. This prevents accidental executions while keeping all resources intact for later.
- Stop the Function App by running this command in Cloud Shell:
- When you are ready to resume, start the Function App again with the corresponding start command.
az functionapp stop --name [[APP_NAME="your function app name"]] --resource-group ContactFuncRG
How do I start it again?
Run the same command with start instead of stop:
az functionapp start --name [[APP_NAME="your function app name"]] --resource-group ContactFuncRG
Delete - I don't want to use this again
Remove all project resources and start fresh. Deleting the resource group removes the Function App, Storage Account, and Application Insights instance in one command.
- Delete the entire resource group by running this command in Cloud Shell:
az group delete --name ContactFuncRG --yes --no-wait
What does this command do?
The --yes flag skips the confirmation prompt. The --no-wait flag returns control immediately while Azure continues deleting resources in the background.
This single command removes the Function App, Storage Account, Application Insights instance, and the resource group itself.
- Confirm deletion is in progress by running:
az group list --query "[?name=='ContactFuncRG'].name" -o tsv
If the group is still deleting, you may see ContactFuncRG briefly. Once deletion completes, the command returns no output.
That's a Wrap!
That's a Wrap!
Nice work! You just shipped a production-ready serverless API endpoint on Azure without installing a single tool on your local machine.
You've learned how to:
- Created a Function App on the Flex Consumption plan from Azure Cloud Shell.
- Deployed a Node.js HTTP-triggered contact endpoint without managing any servers.
- Experienced the security gap of an unprotected endpoint and fixed it with function-level access keys.
- Connected Application Insights and watched live request telemetry flow through Live Metrics.
- (Secret Mission) Added input validation returning descriptive 400 errors for malformed requests.
Ready to quiz yourself?