How to build an HTTP-triggered function with Azure Functions
Introduction
Serverless computing is an architecture that allows developers to deploy code without provisioning and maintaining the backend infrastructure to support it. By abstracting servers, it allows us to focus on the code that we write and think less about hardware.
There are many serverless solutions from cloud computing providers such as Amazon Web Services, Google Cloud, and Microsoft Azure. In this tutorial, I'll show you how to create a serverless function with Azure Functions that's triggered with an HTTP request.
Prerequisites
- Node.js installed
- An Azure account (you can create an account for free here if you don't have one)
I'll show you how to write a function in TypeScript, so you should be familiar with TypeScript or JavaScript. Additionally, you should have experience with NPM, web APIs, and basic UNIX commands.
What is Azure Functions?
Azure Functions is a serverless service from Microsoft that allows us to write blocks of code called functions that run in response to an event, such as an HTTP request or IoT event. Instead of worrying about server operating systems and hardware that host our code, Azure handles the provisioning and maintenance of the hardware and infrastructure needed for us––all we do is write the function logic and deploy it.
Functions will automatically scale up and down based on demand, and Azure will only bill us for the time that our code is executed. This can help us save costs if our application has significant downtime: with traditional hosting, we would have to pay for a server that is always online.
Installing Azure packages for local development
To develop and deploy functions locally, we need to download two packages from Azure. Run the following commands to install Azure Functions Core Tools, a set of tools for developing and testing functions locally (installation instructions for Windows and Linux can be found here):
brew tap azure/functions brew install azure-functions-core-tools@3
You can ensure that Core Tools is installed by running func --version
which should output a version number like 3.x.x
. Next, install Azure CLI (installation instructions for Windows and Linux can be found here):
brew install azure-cli
The Azure CLI provides commands for working with Azure resources and allows us to deploy our function to Azure.
Confirm that the CLI has been installed and sign in to Azure:
az login
If the Azure CLI is installed correctly, you'll be redirected to a page where you can sign in to your Azure account.
Creating the local function project
Now that we have the required packages installed, let's create a function project:
func init azure-tutorial --typescript
cd azure-tutorial
This command creates a local function project called azure-tutorial
with some default configuration files. A function project contains one or more functions that share the same configuration.
Running ls
will show the following files:
azure-tutorial/ ├─ host.json ├─ local.settings.json ├─ package.json ├─ tsconfig.json
Let's briefly explore each of these:
- host.json: Contains global configuration options shared by all of the functions in a function project. We won't be modifying the defaults in this tutorial, so you can read the host.json reference from Microsoft for more information.
- local.settings.json: Stores local app and development tool settings. App settings and secrets, such as API keys and connection strings, can be added to the
Values
object and read in the function as environment variables. See the Microsoft docs for more information about the other supported local settings. - package.json: Contains the project's configuration. Note that we're given a
build
script to compile the TypeScript, aprestart
script that runsbuild
, and astart
script that runs the function locally. - tsconfig.json: Default TypeScript compiler options.
Create the function from a template
Throughout this tutorial, we'll create a function named CreateUser
that imitates a user registration endpoint. It will be triggered by a POST request containing username
and password
fields in the request body. Run the following command to scaffold a CreateUser
function from the HTTP trigger template:
func new --name CreateUser --template "HTTP trigger" --authlevel "anonymous"
Running ls
again will show a new function directory called CreateUser
:
azure-tutorial/ ├─ CreateUser/ │ ├─ function.json │ ├─ index.ts ├─ host.json ├─ local.settings.json ├─ package.json ├─ tsconfig.json
This directory contains everything we need for a function: a function.json
configuration file and the function's code. Open up your azure-tutorial
directory in a text editor so we can start exploring these function files.
function.json
A function must have a function.json file, which contains its configuration information such as the trigger type and bindings. A function has exactly one trigger, which is an event that causes it to run.
A binding defines how we get data into our function or send it out of our function––it connects resources to the function. Bindings are added to the bindings
array. For example, our function contains the following HTTP trigger input binding:
{
"authLevel": "Anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"],
},
The direction
defines it to be an input binding, and name
defines the name of the parameter passed to the function that contains the HTTP request data. Note that we've specified an anonymous
authorization level to allow it to be triggered by anyone. You can read more about authorization levels in the Microsoft docs.
Similarly, we have an output binding that defines our output data:
{
"type": "http",
"direction": "out",
"name": "res"
}
Here, name
defines the property name on the context object returned from our function that contains the response data, which we will see shortly.
index.ts
This file exports a function named httpTrigger
that Azure runs when the function is triggered. The HTTP request object is the req
parameter (defined by the input binding's name
property in function.json
), and a context object is passed through the context
parameter when the function is run.
The default function code provided for us allows us to specify a name in the query parameters or request body. When run, it creates a response message and sets the response on the context object's res
property, which was defined in function.json
.
Testing the default function locally
The boilerplate that we've just explored is a working function that we can make requests to locally. Let's ensure that everything is working properly by running it and making a request to its CreateUser
endpoint.
In the root directory, install the project's NPM dependencies:
npm i
We can now start the function with npm start
, which will compile the TypeScript and start a local server. This command should show the following output:
... Functions: CreateUser: [GET,POST] http://localhost:7071/api/CreateUser ...
By default, HTTP-triggered functions are available at the /api/<function name>
endpoint. Make a test GET request to our function with the following curl
command:
curl http://localhost:7071/api/CreateUser?name=<your name>
This request should return a response like "Hello, Zach. This HTTP triggered function executed successfully." We can also test out the POST request with this command:
curl -i http://localhost:7071/api/CreateUser -d '{"name": "<Your name>"}'
Great! You've created a local function project with a working function. Let's see what else we can do with our HTTP trigger by updating it to do something a little more useful.
Configuring the function to accept POST requests
By default, our HTTP input trigger accepts both GET and POST methods, but our CreateUser
function should only allow a POST request. Let's fix this by removing the GET method from the input binding's method
property, which is an array containing the function's allowed HTTP methods:
{
"bindings": [
{
"authLevel": "Anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["post"]
},
...
],
...
}
While we're here, let's make the name of our function's endpoint RESTful and name it /api/users
instead of /api/CreateUser
. We can specify a custom name by setting the route
property of the input trigger:
{
"bindings": [
{
"authLevel": "Anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["post"],
"route": "users"
},
...
],
...
}
After restarting the function, you should see output to indicate that the CreateUser
function is triggered by the /api/users
endpoint now and only accepts POST requests:
... Functions: CreateUser: [POST] http://localhost:7071/api/users ...
Writing and testing the function
We've got our function bindings configured, so let's replace the generated function with our own code. As mentioned earlier, our function will take in a username
and password
in the request body. We'll also want to return the created user object as JSON and send a 201
response code to indicate that a user was created.
In index.ts
, rename the function from httpTrigger
to createUser
and delete all of the code within the function body. Your file should look something like this:
import { AzureFunction, Context, HttpRequest } from '@azure/functions';
const createUser: AzureFunction = async function (
context: Context,
req: HttpRequest
): Promise<void> {
// TODO: Implement
};
export default createUser;
Next, add the following code to the function body:
const { username, password } = req.body;
const user = { username, password };
// In real life, we'd add the user object to our database here
context.res = {
status: 201,
headers: {
'Content-Type': 'application/json',
},
body: user,
};
We simply construct a user object from the request body and return that object in the response body with the appropriate status code and header. Restart the function and make a POST request with a username and password:
curl -i http://localhost:7071/api/users -d '{"username": "foo", "password": "bar"}'
The response will have {"username": "foo", "password": "bar"}
in the body and a 201
status code.
In real life, though, we'd probably want to add some request validation that requires the caller to provide both a username and password. If the user provides invalid input, we should return a 400
status code and a helpful error message. Replace the function body with this updated implementation:
let responseStatus: number;
let responseBody: {
user?: { username: string; password: string };
error?: string;
};
const { username, password } = req.body;
if (username && password) {
responseStatus = 201;
responseBody = { user: { username, password } };
} else {
responseStatus = 400;
responseBody = { error: 'Missing username or password.' };
}
context.res = {
status: responseStatus,
headers: {
'Content-Type': 'application/json',
},
body: responseBody,
};
Now, restart the function and make a request without a username or password. For example:
curl -i http://localhost:7071/api/users -d '{"username": "foo"}'
You should get a 400
response code and a response containing {"error": "Missing username or password"}
.
That's it for the function. Now, let's get ready to deploy the function to Azure.
Creating Azure resources for deployment
To deploy our function, we need to create an Azure resource group, storage account, and function app. A resource group is a container for Azure resources, and a storage account stores a function's state, code, and configuration files.
You'll want to create the resource group and storage account in a region that's close to you to reduce latency. You can list the regions available to you by running az account list-locations
:
az account list-locations --query "[].{Region:name}" --out table | less
For instance, I live in Washington, United States, so I'll use the westus2
location.
The next few commands require us to repeat a lot of configuration information, so set the following environment variables that we can use later on:
REGION=<your region> # The region you chose above
RESOURCE_GROUP_NAME=azure-tutorial-rg # The name of the resource group
STORAGE_ACCOUNT_NAME=<your unique name> # The name of the storage account
FUNCTION_NAME=<your unique name> # The name of the function app in Azure
Create a resource group with the following command:
az group create --name $RESOURCE_GROUP_NAME --location $REGION
Next, create the storage account:
az storage account create --name $STORAGE_ACCOUNT_NAME --location $REGION --resource-group $RESOURCE_GROUP_NAME --sku Standard_LRS
Note that storage account names need to be globally unique; an ambiguous error message could mean that the storage name is unavailable. You can check if your name is available by running az storage account check-name --name $STORAGE_ACCOUNT_NAME
.
Now we can create our function app in Azure:
az functionapp create --name $FUNCTION_NAME --resource-group $RESOURCE_GROUP_NAME --storage-account $STORAGE_ACCOUNT_NAME --consumption-plan-location $REGION --runtime node --runtime-version 12 --functions-version 3
Like storage accounts, function names need to be unique in Azure.
When the command completes, you can ensure that the function was created by listing your function apps:
az functionapp list --out table
Deploying the function to Azure
We're now ready to deploy our function! First, create a production build of our app:
npm run build:production
We can now deploy the function with the following command:
func azure functionapp publish $FUNCTION_NAME
Once the deployment succeeds, you should see the following output:
... Functions in <function name>: CreateUser - [httpTrigger] Invoke url: https://<function name>.azurewebsites.net/api/users
The "Invoke url" in the output is the URL that triggers our function. Copy the URL and test out the requests we made locally:
curl -i <invoke URL> -d '{"username": "foo", "password": "bar"}' # returns a 201
curl -i <invoke URL> -d '{"username": "foo"}' # returns a 400
Conclusion
Congrats! You've created and deployed your own function to Azure. Now that you've used an HTTP trigger, I encourage you to check out the Azure Functions documentation and explore other types of function triggers.
Let's connect
Come connect with me on LinkedIn, Twitter, and GitHub!
If you found this post helpful, please consider supporting my work financially:
☕️Buy me a coffee!References
- Azure Functions University | Deployment to Azure
- Microsoft | Azure Functions developer guide
- Microsoft | Azure Functions HTTP trigger
- Microsoft | Azure Functions triggers and bindings concepts
- Microsoft | Code and test Azure Functions locally
- Microsoft | Quickstart: Create a TypeScript function in Azure from the command line
- Microsoft | Work with Azure Functions Core Tools
- Microsoft Azure | Go serverless: Event-driven applications with Azure Functions | Azure Friday
- Serverless on Azure | Azure Functions University - HTTP (TypeScript)