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, a prestart script that runs build, and a start 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


Subscribe for the latest news

Sign up for my mailing list to get the latest blog posts and content from me. Unsubscribe anytime.