Introduction
In this article, we will use HTTP-triggered Azure Functions to create a REST API. We will create HTTP-triggered Azure Functions with their default routes, and that means when we test locally, they'll have a URL that looks like this with a route of:
http://localhost:{port}/api/{FunctionName}
And when we deploy them to Azure, we'll have an azurewebsites.net domain name, which includes our Function App name, and the function itself will have the same route:
http://{FunctionAppName}.azurewebsites.net/api/{FunctionName}
And for many scenarios, routes named like this are absolutely fine, for example, if our function is just handling a webhook callback from an external system, the exact route is really quite unimportant. But sometimes we would like more control over the routes for our functions, and that's especially true if we'd like to expose a REST-style API. So, in this article, we're going to create the Function App in Visual Studio and expand it to implement a REST API for a simple application that manages a Task list. It will be used to perform the following operations.
Operations
|
Default Route
|
REST Route
|
List all Tasks
|
api/GetAllTask
|
GET api/task
|
List Task by id
|
api/GetTaskById
|
GET api/task/{id}
|
Create new Task
|
api/CreateTask
|
POST api/task
|
Update a Task
|
api/UpdateTask
|
PUT api/task/{id}
|
Delete a Task
|
api/DeleteTask
|
DELETE api/task /{id}
|
So, let's dive into the code and see how we can implement these five methods using these routes.
Road Map
- Creating Azure Function Project in Visual Studio 2019
- Creating Model Classes
- Task
- TaskCreate
- TaskUpdate
- Create Task method
- Get All Task method
- Get Task By Id method
- Update Task method
- Delete Task method
- Testing API’s with Postman
- Use Cases
Creating Azure Function Project in Visual Studio 2019
- Open Visual Studio 2019
- Click "Create a new project"
- Select Azure Functions Teamplate
Note
Make sure you have Azure workloads installed.
In Configure, your new project provides the below information. Here, click "Create".
- Project name: Any meaningful name; I’m using RestFuncApp
- Location: Provide a path for the project
- Solution name: same as the project name
Creating Model Classes
I've also created a file called Models which has got three class definitions in here at the moment. The first is the Task entity, which has an Id, which I initialized with a new GUID; a CreatedTime, which is initialized with a current time; and then it's got a TaskDescription and an IsCompleted flag.
Now when we create a new Task item, we could allow the caller of our API to just pass in an entire serialized instance of this Task class, but actually I don't want the caller of my API to be able to choose the Id or override the created date, and it's actually a good practice from a security perspective to only allow the caller of your APIs to specify the properties that you explicitly allow. So, what I've done is I've made a very simple TodoCreateModel class that defines the one and only property that you're allowed to set when you're creating a new task, and that's the description. Everything else will get its default values.
I've defined a custom entity called TaskUpdateModel that contains only the fields that I want users to be allowed to change. And so, we can see the definition of the TaskUpdateModel class here, and it's simply going to allow callers to update the TaskDescription and change the IsCompleted flag.
Create Task method
I have renamed Function1.cs file and class to TaskApi. Although the tooling likes to create a new class for each function, it's perfectly possible for you to organize your functions how you want, and actually I think for this API it makes sense for me to keep all the functions together in a single class. I'll delete our old Function1 function and create my own as shown in below image.
So, first of all, here's our FunctionName attribute, which I set to CreateTask. Now we're going to override the route, so this is just a name for this Azure Function, and I've also chosen to give the method the same name, although it's not required that you do that. Now this is an asynchronous method, and it returns a task of IActionResult. We are taking an HTTP request parameter, which is marked up with an HttpTrigger attribute. I'm still specifying the Authorization Level as Anonymous, but now I'm only allowing a single HTTP method, the post method, and this is the REST convention for creating a new resource.
Inside our method, I'm starting off by writing a message to the log, and then we're reading the body of the incoming HTTP request into a string, and we're using Newtonsoft. Json to deserialize that into an instance of our TaskCreateModel class. I create a new Task item, and I copy the deserialized description from our HTTP request body into it. Next, this is the point at which we would normally add our Task item into a database, but we don't have a database yet, so for now I'm just going to use an in-memory list. So, I'll create a static list of items of type Task, as shown in line# 17 and we'll put our new item into that list. I’m using this static list will demonstrate something quite important. and that's that the Azure Functions runtime doesn't completely shut down and restart between requests to our function, so it is possible for state to be maintained between calls to functions. If we call this function twice in short succession, we will get two items in this list.
However, if you're running in the cloud, the Azure Functions runtime will sometimes shut down if your Function App hasn't been used for a while, so it would just forget the Task items if we tried to use this technique. Also, the Azure Functions runtime can scale automatically, so there could be multiple instances running, in which case there would be multiple instances of this static list. So, this is absolutely not a technique that you'd use in a real application, but it's fine for now while we build out this API, and we can switch to a real database later. Finally, we'll use the OkObjectResult to return the entire Task item, which will be serialized to JSON for us. And that's our first function done.
Get All Tasks method
This function will get all the Task items. It will have exactly the same route as the create function, so /api/task. The FunctionName is GetAllTasks, it's Anonymous authorization again, and the route is exactly the same as our CreateTask function. However, the HTTP method we support is get. Then, in the body of the method things are really straightforward. I'm simply logging a message to the TraceWriter, and then I'm using the OkObjectResult to return the entire contents of our in-memory Tasks list as a serialized JSON array.
Get Task by Id method
This function will get a Task item by its id. The route will be api/task /, and then the id of the Task item, and it will respond to the HTTP GET method. The FunctionName is GetTaskById. We're responding again to the HTTP get method, but notice that the route is task/id, and this special id in curly bracket syntax means take anything in this part of the route and consider it to be the id. But how can we actually use that id in our code? Well, I've added another parameter to this function with the type of string and the name of id, and that allows us in the body of our method to use the id to look up in our Task Items list the item that has a matching id. Of course, it's possible that it doesn't exist, so we return a NotFoundResult if that happens, which is an HTTP 404. If we did find it, then we can use the OkObjectResult again to return a 200 OK with the serialized Task item in the body.
Update Task method
This function will update existing Task items using the api/task /id route, and using the HTTP PUT method. Now, we saw in the last function how we could put an id inside a route, and so we're going to use exactly the same technique for this method. The supported HTTP method is put, and the Route is task/id in curly brackets, just like we saw for the GetById method. We've also got a string id parameter again, allowing us to access the value of the id within the function. Now in this function there is a bit more work to do. First, we need to check if the item to be updated actually exists, and if it doesn’t, I'm going to return a NotFoundResult. Then we're going to deserialize the body of the PUT request to get the values of the fields that we want to update. And, you'll notice that I'm not deserializing an entire Task entity. I don't actually want the caller of this method to be able to update any field, such as the id or created date. So just like I did with the Create function, I've defined a custom entity called TaskUpdateModel that contains only the fields that I want users to be allowed to change. And so, we can see the definition of the TaskUpdateModel class here, and it's simply going to allow callers to update the TaskDescription and change the IsCompleted flag. Once I've deserialized the updated values, I use them to update the actual Task item from our in-memory list, and I've decided to make providing an updated description optional to simplify using this method when all you want to do is toggle the IsCompleted status. Finally, I return the new status of the entire Task item in an OkObjectResult.
Delete Task method
This function will delete an existing Task item using the route api/task/id, and using the HTTP DELETE method. By now I'm hoping you can pretty much already guess what this function is going to look like. It's got a name of DeleteTask, it responds to the delete HTTP method, and it has a route of task/id. Again, we have a string id parameter, as we need to make use of the id in our method. Inside the method, we check for the existence of the item, returning a 404 NotFoundResult if we couldn't find it, and if we did, we're simply removing it from our in-memory list and we're returning an OkResult. Notice this isn't an OkObjectResult like all our other APIs, and that's because the body of this response is empty. We've got nothing to return other than a simple 200 OK indicating that we've deleted this Task item.
Testing API’s with Postman
I’m using Postman to test our REST API’s. First, I'm going to build and run our Web API. We'll wait for the Functions runtime to start up, and once it's completed, we can see the list of all the functions that it's found in our Function App. As you can see, each of the five functions in our API is listed here along with the local URL that we can call them on, and by default that's on localhost port 7071.
Now, Postman is a free utility that makes it really easy to construct HTTP requests to test out APIs. I've loaded Postman, and first I'm going to GetAllTasks, which is simply a GET request to api/task. And as you can see, we get back an empty array, which is what we expect. There's no items in our list yet.
So, let's create a new one. I'll post to that same URL, and in the body, I’ll set it up to be a JSON payload. And then I'll define an object that has a TaskDescription property, and the ‘Blog post’ will be My first in-memory task. When we send this request, if we scroll down, we can see that we get back a 200 OK response, and it shows us the details of the newly created Task item, including its id.
I'll copy that id to the clipboard, as we're going to need it for our next method. So now let's set that Task item to be completed. I'll need to add the id of the newly created Task item to the URL that we're calling, and the HTTP method now needs to be PUT. In the body, I'm defining a JSON object that has an IsCompleted flag set to true. And when we send this, we see that we get a 200 OK, and the response shows the entire state of the updated item with its IsCompleted flag now set to true.
So now let's try and use the GetTaskById function. We'll call the Task endpoint with the id in the route and the GET method, and we get a 200 OK again with the response body showing the status of that Task item.
Finally, let's delete this Task item. I'll use the DELETE method and the id of our Task item, and as you can see, we get a 200 OK back.
Use Cases
- Use Azure Functions as workflow orchestrator.
- Run your logic/code only when needed; i.e., consumption plan
- Automating business processes
- Use as B2B API
- APIs for something like a conference/concert which will run hot during the event, and cold otherwise (but might need to hang around).
- Infrequent tasks (Hourly, Daily, Weekly, etc.) such as DB clean up
- IoT Data processing where usage is high during the work day, cold otherwise
- Custom processing steps during a Logic App
- Threshold alerts coming from Stream Analytics + IoT Hub
Code Link