Getting Started with Azure Functions: Building a Simple Product CRUD API

|

1. Verify if Azure CLI is Installed

Run the following command to check if Azure CLI is installed:

which az

If nothing is returned, the CLI is not installed. Proceed to install it.


2. Install Azure CLI

On macOS (using Homebrew)

If you’re on macOS, install Azure CLI using Homebrew:

brew update && brew install azure-cli

If you don’t have Homebrew installed, first run:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

On Linux

Follow the official instructions here: Install Azure CLI on Linux.

On Windows

You can install Azure CLI using the MSI installer from the official website: Install Azure CLI.

Starting your CRUD in Azure Functions

To begin creating your product CRUD with Azure Functions, you’ll first need to set up your environment and execute specific commands in the terminal. Make sure you have the Azure CLI, Azure Functions Core Tools, and the appropriate runtime (e.g., Node.js) installed.

Start by logging into Azure. Run the following command to authenticate your account:

az login

This will open a browser where you can log in. After authentication, navigate to the directory where you want to create your project. Create a folder for your CRUD application and move into it:

mkdir azure-functions-samplecd azure-functions-sample

Azure Functions Runtime

In this example, we will be using Node.js as the Azure Functions runtime.
If you do not have node installed, install it first. If you already have it, skip this step.

First, ensure you install a Node.js version compatible with Azure Functions. As of the time of writing, the recommended version is Node.js 18.

On Mac

Using Homebrew: Open Terminal and run:

brew install node@18
echo 'export PATH="/opt/homebrew/opt/node@18/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Verify installation:

node -v npm -v

On Windows

Download the installer:

Visit Node.js Downloads.

Choose the LTS version and download the .msi file.

Run the installer:

Follow the installation prompts and ensure Add to PATH is selected.

Verify installation: Open Command Prompt and run:

node -v npm -v

On Linux

Using NodeSource (recommended for most distros):

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs 

This works for Ubuntu/Debian. For other distros, check NodeSource.

Verify installation:

node -v npm -v

Initializing the project

Initialize the Azure Functions project with the desired runtime. For example, if you’re using Node.js, use the following command:

func init azure-functions-sample --worker-runtime node

This creates the project structure. To add functions for your CRUD operations, start with the func new command. When prompted, select the HTTP Trigger template and name your function (e.g., CreateProduct). Repeat this step for other operations like reading, updating, and deleting products.

For instance, create the function:

func new 

For node choose option 3: node.

For language you can choose javascript or typescript. In this example we’ll choose 2: typescript.

Choose option 8 for the HTTP trigger template, and name it “product”.

Build and run locally

To be able to run the project, first you must build it:

npm run build

Then you can alreay run the project:

func start

Or you can use the following command to do both:

npm run build & func start

The product API

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";

type Product = {
    id: string;
    name: string;
    price: number;
};

const products: Record<string, Product> = {};

export async function product(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`HTTP function processed request for url "${request.url}"`);

    const method = request.method.toUpperCase();
    const productId = request.query.get("id");

    switch (method) {
        case "GET": {
            if (productId) {
                const product = products[productId];
                if (product) {
                    return { body: JSON.stringify({ success: true, data: product }), status: 200 };
                } else {
                    return { body: JSON.stringify({ success: false, message: `Product with ID "${productId}" not found.` }), status: 404 };
                }
            } else {
                return { body: JSON.stringify({ success: true, data: Object.values(products) }), status: 200 };
            }
        }
        case "POST": {
            const { id, name, price } = (await request.json()) as Product;
            if (!id || !name || !price) {
                return { body: JSON.stringify({ success: false, message: "Invalid product data. 'id', 'name', and 'price' are required." }), status: 400 };
            }
            products[id] = { id, name, price };
            return { body: JSON.stringify({ success: true, message: `Product with ID "${id}" created.` }), status: 201 };
        }
        case "PUT": {
            if (!productId) {
                return { body: JSON.stringify({ success: false, message: "Product ID is required for updating." }), status: 400 };
            }
            const { name, price } = (await request.json()) as Partial<Product>;
            const product = products[productId];
            if (!product) {
                return { body: JSON.stringify({ success: false, message: `Product with ID "${productId}" not found.` }), status: 404 };
            }
            if (name) product.name = name;
            if (price) product.price = price;
            return { body: JSON.stringify({ success: true, message: `Product with ID "${productId}" updated.` }), status: 200 };
        }
        case "DELETE": {
            if (!productId) {
                return { body: JSON.stringify({ success: false, message: "Product ID is required for deletion." }), status: 400 };
            }
            const product = products[productId];
            if (!product) {
                return { body: JSON.stringify({ success: false, message: `Product with ID "${productId}" not found.` }), status: 404 };
            }
            delete products[productId];
            return { body: JSON.stringify({ success: true, message: `Product with ID "${productId}" deleted.` }), status: 200 };
        }
        default: {
            return { body: JSON.stringify({ success: false, message: `Method ${method} not allowed.` }), status: 405 };
        }
    }
}

app.http('product', {
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    authLevel: 'anonymous',
    handler: product
});

This code implements an Azure Function that provides a basic CRUD (Create, Read, Update, Delete) functionality for managing products. Instead of using a database, it relies on a simple in-memory array (products) for storing data, making it suitable for tutorials or initial learning purposes without introducing database complexity.


How It Works

1. Data Storage

The products variable is a simple JavaScript object (a dictionary) used to simulate a database. Each product is stored as a key-value pair, where the id acts as the key and the product data is the value.

const products: Record<string, Product> = {};

2. HTTP Methods

The function supports four HTTP methods:

  • GET: Fetches products (all or by id).
  • POST: Creates a new product.
  • PUT: Updates an existing product.
  • DELETE: Deletes a product by id.

Code Breakdown

The code provides a simple CRUD API for managing products using an in-memory object as the data store. It supports four HTTP methods: GET, POST, PUT, and DELETE. Each method performs specific operations and responds with structured JSON, ensuring clarity and consistency.


GET Method

The GET method retrieves one or all products. If an id is specified in the query string, the function fetches the product with that ID. If no id is provided, it returns all products.

Code:

case "GET": {
    if (productId) {
        const product = products[productId];
        if (product) {
            return { body: JSON.stringify({ success: true, data: product }), status: 200 };
        } else {
            return { body: JSON.stringify({ success: false, message: `Product with ID "${productId}" not found.` }), status: 404 };
        }
    } else {
        return { body: JSON.stringify({ success: true, data: Object.values(products) }), status: 200 };
    }
}

Example Requests:

Fetch all products:
GET /api/product{ "success": true, "data": [ { "id": "1", "name": "Product A", "price": 10.0 }, { "id": "2", "name": "Product B", "price": 15.0 } ] }
Fetch a single product:
GET /api/product?id=1{ "success": true, "data": { "id": "1", "name": "Product A", "price": 10.0 } }

POST Method

The POST method creates a new product. The request must include id, name, and price in the JSON body. If any required field is missing, an error is returned.

Code:

case "POST": {
    const { id, name, price } = (await request.json()) as Product;
    if (!id || !name || !price) {
        return { body: JSON.stringify({ success: false, message: "Invalid product data. 'id', 'name', and 'price' are required." }), status: 400 };
    }
    products[id] = { id, name, price };
    return { body: JSON.stringify({ success: true, message: `Product with ID "${id}" created.` }), status: 201 };
}

Example Requests:

Create a product:
POST /api/product
Body:{ "id": "3", "name": "Product C", "price": 20.0 } Response:{ "success": true, "message": "Product with ID '3' created." }
Error example:
Body:{ "name": "Product D" } Response:{ "success": false, "message": "Invalid product data. 'id', 'name', and 'price' are required." }

PUT Method

The PUT method updates an existing product. The product ID must be specified in the query string, and the updated details must be included in the request body. If the product does not exist, an error is returned.

Code:

case "PUT": {
    if (!productId) {
        return { body: JSON.stringify({ success: false, message: "Product ID is required for updating." }), status: 400 };
    }
    const { name, price } = (await request.json()) as Partial<Product>;
    const product = products[productId];
    if (!product) {
        return { body: JSON.stringify({ success: false, message: `Product with ID "${productId}" not found.` }), status: 404 };
    }
    if (name) product.name = name;
    if (price) product.price = price;
    return { body: JSON.stringify({ success: true, message: `Product with ID "${productId}" updated.` }), status: 200 };
}

Example Requests:

Update a product:
PUT /api/product?id=1
Body:{ "name": "Updated Product A", "price": 12.0 } 
Response:{ "success": true, "message": "Product with ID '1' updated." }
Error example:
PUT /api/product?id=999
Response:
{ "success": false, "message": "Product with ID '999' not found." }

DELETE Method

The DELETE method removes a product based on its ID. If the product does not exist, an error is returned.

Code:

case "DELETE": {
    if (!productId) {
        return { body: JSON.stringify({ success: false, message: "Product ID is required for deletion." }), status: 400 };
    }
    const product = products[productId];
    if (!product) {
        return { body: JSON.stringify({ success: false, message: `Product with ID "${productId}" not found.` }), status: 404 };
    }
    delete products[productId];
    return { body: JSON.stringify({ success: true, message: `Product with ID "${productId}" deleted.` }), status: 200 };
}

Example Requests:

Delete a product:
DELETE /api/product?id=1{ "success": true, "message": "Product with ID '1' deleted." }
Error example:
DELETE /api/product?id=999{ "success": false, "message": "Product with ID '999' not found." }

This structure ensures the API is easy to use, with clear error handling and consistent JSON responses for all methods.


Why Use an Array?

This approach simplifies the setup by avoiding database dependencies. The products object is stored in memory, meaning all data will be lost when the function restarts. This is ideal for tutorials, testing, and scenarios where you want to focus on the basics of Azure Functions.


Next Steps

Once you’re comfortable with this, you can extend the implementation to connect to a database like Azure Cosmos DB, SQL, or MongoDB to persist data across sessions.